python-codex 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
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.
- pycodex/__init__.py +139 -2
- pycodex/agent.py +290 -0
- pycodex/cli.py +641 -0
- pycodex/collaboration.py +21 -0
- pycodex/context.py +580 -0
- pycodex/doctor.py +360 -0
- pycodex/model.py +533 -0
- pycodex/prompts/collaboration_default.md +11 -0
- pycodex/prompts/collaboration_plan.md +128 -0
- pycodex/prompts/default_base_instructions.md +275 -0
- pycodex/prompts/exec_tools.json +411 -0
- pycodex/prompts/models.json +847 -0
- pycodex/prompts/permissions/approval_policy/never.md +1 -0
- pycodex/prompts/permissions/approval_policy/on_failure.md +1 -0
- pycodex/prompts/permissions/approval_policy/on_request.md +57 -0
- pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +33 -0
- pycodex/prompts/permissions/approval_policy/unless_trusted.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/read_only.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/workspace_write.md +1 -0
- pycodex/prompts/subagent_tools.json +163 -0
- pycodex/protocol.py +347 -0
- pycodex/runtime.py +200 -0
- pycodex/runtime_services.py +408 -0
- pycodex/tools/__init__.py +58 -0
- pycodex/tools/agent_tool_schemas.py +70 -0
- pycodex/tools/apply_patch_tool.py +363 -0
- pycodex/tools/base_tool.py +168 -0
- pycodex/tools/close_agent_tool.py +55 -0
- pycodex/tools/code_mode_manager.py +519 -0
- pycodex/tools/exec_command_tool.py +96 -0
- pycodex/tools/exec_runtime.js +161 -0
- pycodex/tools/exec_tool.py +48 -0
- pycodex/tools/grep_files_tool.py +150 -0
- pycodex/tools/list_dir_tool.py +135 -0
- pycodex/tools/read_file_tool.py +217 -0
- pycodex/tools/request_permissions_tool.py +95 -0
- pycodex/tools/request_user_input_tool.py +167 -0
- pycodex/tools/resume_agent_tool.py +56 -0
- pycodex/tools/send_input_tool.py +106 -0
- pycodex/tools/shell_command_tool.py +107 -0
- pycodex/tools/shell_tool.py +112 -0
- pycodex/tools/spawn_agent_tool.py +97 -0
- pycodex/tools/unified_exec_manager.py +380 -0
- pycodex/tools/update_plan_tool.py +79 -0
- pycodex/tools/view_image_tool.py +111 -0
- pycodex/tools/wait_agent_tool.py +75 -0
- pycodex/tools/wait_tool.py +68 -0
- pycodex/tools/web_search_tool.py +30 -0
- pycodex/tools/write_stdin_tool.py +75 -0
- pycodex/utils/__init__.py +40 -0
- pycodex/utils/dotenv.py +64 -0
- pycodex/utils/get_env.py +218 -0
- pycodex/utils/random_ids.py +19 -0
- pycodex/utils/visualize.py +978 -0
- python_codex-0.1.0.dist-info/METADATA +267 -0
- python_codex-0.1.0.dist-info/RECORD +60 -0
- python_codex-0.1.0.dist-info/entry_points.txt +2 -0
- python_codex-0.1.0.dist-info/licenses/LICENSE +201 -0
- python_codex-0.0.1.dist-info/METADATA +0 -30
- python_codex-0.0.1.dist-info/RECORD +0 -4
- {python_codex-0.0.1.dist-info → python_codex-0.1.0.dist-info}/WHEEL +0 -0
pycodex/doctor.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import socket
|
|
6
|
+
import ssl
|
|
7
|
+
import time
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from dataclasses import asdict, dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from .model import ResponsesModelClient, ResponsesProviderConfig
|
|
15
|
+
from .protocol import AssistantMessage, Prompt, UserMessage
|
|
16
|
+
from .utils.dotenv import DOTENV_FILENAME, load_codex_dotenv
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True)
|
|
20
|
+
class DoctorCheck:
|
|
21
|
+
name: str
|
|
22
|
+
ok: bool
|
|
23
|
+
detail: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class DoctorReport:
|
|
28
|
+
ok: bool
|
|
29
|
+
config_path: str
|
|
30
|
+
dotenv_path: str
|
|
31
|
+
profile: str | None
|
|
32
|
+
provider_name: str | None = None
|
|
33
|
+
model: str | None = None
|
|
34
|
+
base_url: str | None = None
|
|
35
|
+
responses_url: str | None = None
|
|
36
|
+
api_key_env: str | None = None
|
|
37
|
+
api_key_loaded: bool = False
|
|
38
|
+
checks: list[DoctorCheck] = field(default_factory=list)
|
|
39
|
+
live_output_text: str | None = None
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, object]:
|
|
42
|
+
return asdict(self)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_doctor_parser():
|
|
46
|
+
import argparse
|
|
47
|
+
|
|
48
|
+
parser = argparse.ArgumentParser(
|
|
49
|
+
prog="pycodex doctor",
|
|
50
|
+
description="Diagnose pycodex config, auth, and provider connectivity.",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--config",
|
|
54
|
+
default=str(Path.home() / ".codex" / "config.toml"),
|
|
55
|
+
help="Path to Codex config.toml.",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--profile",
|
|
59
|
+
default=None,
|
|
60
|
+
help="Optional profile name from config.toml.",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--timeout-seconds",
|
|
64
|
+
type=float,
|
|
65
|
+
default=120.0,
|
|
66
|
+
help="Timeout used for network and live model checks.",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--skip-live",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Skip the live Responses API request and only run static/network checks.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--json",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Print the full doctor report as JSON.",
|
|
77
|
+
)
|
|
78
|
+
return parser
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def collect_doctor_report(
|
|
82
|
+
config_path: str | Path,
|
|
83
|
+
profile: str | None = None,
|
|
84
|
+
timeout_seconds: float = 120.0,
|
|
85
|
+
skip_live: bool = False,
|
|
86
|
+
) -> DoctorReport:
|
|
87
|
+
config_file = Path(config_path).expanduser().resolve()
|
|
88
|
+
dotenv_file = config_file.parent / DOTENV_FILENAME
|
|
89
|
+
report = DoctorReport(
|
|
90
|
+
ok=False,
|
|
91
|
+
config_path=str(config_file),
|
|
92
|
+
dotenv_path=str(dotenv_file),
|
|
93
|
+
profile=profile,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
checks = report.checks
|
|
97
|
+
checks.append(
|
|
98
|
+
DoctorCheck(
|
|
99
|
+
"config",
|
|
100
|
+
config_file.is_file(),
|
|
101
|
+
str(config_file) if config_file.is_file() else f"missing: {config_file}",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
checks.append(
|
|
105
|
+
DoctorCheck(
|
|
106
|
+
"dotenv",
|
|
107
|
+
True,
|
|
108
|
+
(
|
|
109
|
+
str(dotenv_file)
|
|
110
|
+
if dotenv_file.is_file()
|
|
111
|
+
else f"missing (optional): {dotenv_file}"
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
if not config_file.is_file():
|
|
116
|
+
return report
|
|
117
|
+
|
|
118
|
+
load_codex_dotenv(config_file)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
provider_config = ResponsesProviderConfig.from_codex_config(config_file, profile)
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
checks.append(DoctorCheck("config_parse", False, f"{type(exc).__name__}: {exc}"))
|
|
124
|
+
return report
|
|
125
|
+
|
|
126
|
+
report.provider_name = provider_config.provider_name
|
|
127
|
+
report.model = provider_config.model
|
|
128
|
+
report.base_url = provider_config.base_url
|
|
129
|
+
client = ResponsesModelClient(provider_config)
|
|
130
|
+
report.responses_url = client.responses_url()
|
|
131
|
+
report.api_key_env = provider_config.api_key_env
|
|
132
|
+
report.api_key_loaded = bool(provider_config.api_key_env and _loaded_api_key(provider_config))
|
|
133
|
+
|
|
134
|
+
checks.append(
|
|
135
|
+
DoctorCheck(
|
|
136
|
+
"provider",
|
|
137
|
+
True,
|
|
138
|
+
(
|
|
139
|
+
f"provider={provider_config.provider_name} "
|
|
140
|
+
f"model={provider_config.model} "
|
|
141
|
+
f"wire_api={provider_config.wire_api}"
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
checks.append(
|
|
146
|
+
DoctorCheck(
|
|
147
|
+
"api_key",
|
|
148
|
+
report.api_key_loaded,
|
|
149
|
+
(
|
|
150
|
+
f"{provider_config.api_key_env} loaded"
|
|
151
|
+
if report.api_key_loaded
|
|
152
|
+
else f"{provider_config.api_key_env} is missing"
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
parsed_url = urllib.parse.urlsplit(client.responses_url())
|
|
158
|
+
host = parsed_url.hostname or ""
|
|
159
|
+
port = parsed_url.port or (443 if parsed_url.scheme == "https" else 80)
|
|
160
|
+
proxies = requests.utils.get_environ_proxies(client.responses_url())
|
|
161
|
+
|
|
162
|
+
checks.append(
|
|
163
|
+
DoctorCheck(
|
|
164
|
+
"proxy",
|
|
165
|
+
True,
|
|
166
|
+
_proxy_detail(proxies),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
addresses = await asyncio.to_thread(
|
|
172
|
+
socket.getaddrinfo,
|
|
173
|
+
host,
|
|
174
|
+
port,
|
|
175
|
+
type=socket.SOCK_STREAM,
|
|
176
|
+
)
|
|
177
|
+
except OSError as exc:
|
|
178
|
+
checks.append(DoctorCheck("dns", False, f"{host}:{port} -> {exc}"))
|
|
179
|
+
return _finalize_report(report)
|
|
180
|
+
|
|
181
|
+
resolved_addresses = sorted(
|
|
182
|
+
{
|
|
183
|
+
result[4][0]
|
|
184
|
+
for result in addresses
|
|
185
|
+
if len(result) >= 5 and isinstance(result[4], tuple) and result[4]
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
checks.append(
|
|
189
|
+
DoctorCheck(
|
|
190
|
+
"dns",
|
|
191
|
+
True,
|
|
192
|
+
f"{host}:{port} -> {', '.join(resolved_addresses) or 'resolved'}",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if proxies:
|
|
197
|
+
checks.append(
|
|
198
|
+
DoctorCheck(
|
|
199
|
+
"transport",
|
|
200
|
+
True,
|
|
201
|
+
"skipped direct probe because requests will use environment proxy settings",
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
tcp_ok, tcp_detail = await asyncio.to_thread(
|
|
206
|
+
_probe_transport,
|
|
207
|
+
parsed_url.scheme,
|
|
208
|
+
host,
|
|
209
|
+
port,
|
|
210
|
+
timeout_seconds,
|
|
211
|
+
)
|
|
212
|
+
checks.append(DoctorCheck("transport", tcp_ok, tcp_detail))
|
|
213
|
+
|
|
214
|
+
if skip_live:
|
|
215
|
+
return _finalize_report(report)
|
|
216
|
+
if not report.api_key_loaded:
|
|
217
|
+
checks.append(DoctorCheck("live", False, "skipped: missing API key"))
|
|
218
|
+
return _finalize_report(report)
|
|
219
|
+
|
|
220
|
+
live_ok, live_detail, live_output_text = await _run_live_check(
|
|
221
|
+
config_file,
|
|
222
|
+
profile,
|
|
223
|
+
timeout_seconds,
|
|
224
|
+
)
|
|
225
|
+
report.live_output_text = live_output_text
|
|
226
|
+
checks.append(DoctorCheck("live", live_ok, live_detail))
|
|
227
|
+
return _finalize_report(report)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def run_doctor_cli(args) -> int:
|
|
231
|
+
report = await collect_doctor_report(
|
|
232
|
+
args.config,
|
|
233
|
+
args.profile,
|
|
234
|
+
args.timeout_seconds,
|
|
235
|
+
args.skip_live,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if args.json:
|
|
239
|
+
print(json.dumps(report.to_dict(), ensure_ascii=False, indent=2))
|
|
240
|
+
else:
|
|
241
|
+
print(format_doctor_report(report))
|
|
242
|
+
|
|
243
|
+
return 0 if report.ok else 1
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def format_doctor_report(report: DoctorReport) -> str:
|
|
247
|
+
lines = [
|
|
248
|
+
f"config: {report.config_path}",
|
|
249
|
+
f"dotenv: {report.dotenv_path}",
|
|
250
|
+
]
|
|
251
|
+
if report.profile is not None:
|
|
252
|
+
lines.append(f"profile: {report.profile}")
|
|
253
|
+
if report.provider_name is not None:
|
|
254
|
+
lines.append(f"provider: {report.provider_name}")
|
|
255
|
+
if report.model is not None:
|
|
256
|
+
lines.append(f"model: {report.model}")
|
|
257
|
+
if report.responses_url is not None:
|
|
258
|
+
lines.append(f"responses_url: {report.responses_url}")
|
|
259
|
+
if report.api_key_env is not None:
|
|
260
|
+
lines.append(f"api_key_env: {report.api_key_env}")
|
|
261
|
+
if report.live_output_text is not None:
|
|
262
|
+
lines.append(f"live_output: {report.live_output_text}")
|
|
263
|
+
for check in report.checks:
|
|
264
|
+
status = "ok" if check.ok else "fail"
|
|
265
|
+
lines.append(f"{check.name}: {status} - {check.detail}")
|
|
266
|
+
lines.append(f"overall: {'ok' if report.ok else 'fail'}")
|
|
267
|
+
return "\n".join(lines)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _loaded_api_key(provider_config: ResponsesProviderConfig) -> str:
|
|
271
|
+
try:
|
|
272
|
+
return provider_config.api_key()
|
|
273
|
+
except Exception:
|
|
274
|
+
return ""
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _probe_transport(
|
|
278
|
+
scheme: str,
|
|
279
|
+
host: str,
|
|
280
|
+
port: int,
|
|
281
|
+
timeout_seconds: float,
|
|
282
|
+
) -> tuple[bool, str]:
|
|
283
|
+
started = time.perf_counter()
|
|
284
|
+
try:
|
|
285
|
+
with socket.create_connection((host, port), timeout=timeout_seconds) as sock:
|
|
286
|
+
if scheme == "https":
|
|
287
|
+
with ssl.create_default_context().wrap_socket(
|
|
288
|
+
sock,
|
|
289
|
+
server_hostname=host,
|
|
290
|
+
):
|
|
291
|
+
pass
|
|
292
|
+
except OSError as exc:
|
|
293
|
+
elapsed = time.perf_counter() - started
|
|
294
|
+
return False, f"{scheme.upper()} {host}:{port} failed after {elapsed:.2f}s: {exc}"
|
|
295
|
+
|
|
296
|
+
elapsed = time.perf_counter() - started
|
|
297
|
+
label = "tls" if scheme == "https" else "tcp"
|
|
298
|
+
return True, f"{label} {host}:{port} connected in {elapsed:.2f}s"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _proxy_detail(proxies: dict[str, str]) -> str:
|
|
302
|
+
if not proxies:
|
|
303
|
+
return "not configured"
|
|
304
|
+
return ", ".join(
|
|
305
|
+
f"{key}={_redact_proxy_url(value)}" for key, value in sorted(proxies.items())
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _redact_proxy_url(value: str) -> str:
|
|
310
|
+
parsed = urllib.parse.urlsplit(value)
|
|
311
|
+
if not parsed.scheme or not parsed.netloc:
|
|
312
|
+
return value
|
|
313
|
+
host = parsed.hostname or ""
|
|
314
|
+
port = f":{parsed.port}" if parsed.port is not None else ""
|
|
315
|
+
netloc = f"{host}{port}"
|
|
316
|
+
return urllib.parse.urlunsplit(
|
|
317
|
+
(parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
async def _run_live_check(
|
|
322
|
+
config_path: Path,
|
|
323
|
+
profile: str | None,
|
|
324
|
+
timeout_seconds: float,
|
|
325
|
+
) -> tuple[bool, str, str | None]:
|
|
326
|
+
client = ResponsesModelClient.from_codex_config(
|
|
327
|
+
config_path,
|
|
328
|
+
profile,
|
|
329
|
+
timeout_seconds,
|
|
330
|
+
originator="pycodex_doctor",
|
|
331
|
+
)
|
|
332
|
+
prompt = Prompt(
|
|
333
|
+
input=[UserMessage(text="Reply with exactly OK.")],
|
|
334
|
+
tools=[],
|
|
335
|
+
parallel_tool_calls=True,
|
|
336
|
+
base_instructions="Reply with exactly OK.",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
started = time.perf_counter()
|
|
340
|
+
try:
|
|
341
|
+
response = await client.complete(prompt)
|
|
342
|
+
except Exception as exc:
|
|
343
|
+
elapsed = time.perf_counter() - started
|
|
344
|
+
return False, f"failed after {elapsed:.2f}s: {type(exc).__name__}: {exc}", None
|
|
345
|
+
|
|
346
|
+
elapsed = time.perf_counter() - started
|
|
347
|
+
output_text = next(
|
|
348
|
+
(
|
|
349
|
+
item.text
|
|
350
|
+
for item in response.items
|
|
351
|
+
if isinstance(item, AssistantMessage)
|
|
352
|
+
),
|
|
353
|
+
None,
|
|
354
|
+
)
|
|
355
|
+
return True, f"completed in {elapsed:.2f}s", output_text
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _finalize_report(report: DoctorReport) -> DoctorReport:
|
|
359
|
+
report.ok = all(check.ok for check in report.checks)
|
|
360
|
+
return report
|