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.
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/PKG-INFO +45 -25
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/README.md +44 -24
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/__init__.py +0 -4
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/async_client.py +103 -22
- codex_python_sdk-0.2.0/codex_python_sdk/policy.py +308 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/sync_client.py +3 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/PKG-INFO +45 -25
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/pyproject.toml +1 -1
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/tests/test_codex_python_sdk.py +172 -10
- codex_python_sdk-0.2.0/tests/test_policy_engine.py +227 -0
- codex_python_sdk-0.1.0/codex_python_sdk/policy.py +0 -636
- codex_python_sdk-0.1.0/tests/test_policy_engine.py +0 -619
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/LICENSE +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/_shared.py +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/errors.py +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/examples/__init__.py +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/examples/demo_smoke.py +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/factory.py +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/renderer.py +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk/types.py +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/SOURCES.txt +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/dependency_links.txt +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/entry_points.txt +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/requires.txt +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/codex_python_sdk.egg-info/top_level.txt +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/setup.cfg +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/tests/test_integration_real.py +0 -0
- {codex_python_sdk-0.1.0 → codex_python_sdk-0.2.0}/tests/test_renderer.py +0 -0
- {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.
|
|
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
|
-
|
|
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: `
|
|
122
|
-
- File change approval: `
|
|
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
|
|
128
|
+
This is production-safer by default, but may block unattended workflows unless you opt into looser hooks.
|
|
127
129
|
|
|
128
|
-
Recommended
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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: `
|
|
91
|
-
- File change approval: `
|
|
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
|
|
97
|
+
This is production-safer by default, but may block unattended workflows unless you opt into looser hooks.
|
|
96
98
|
|
|
97
|
-
Recommended
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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(["
|
|
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
|
-
|
|
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
|
-
|
|
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:
|