python-codex 0.1.1__py3-none-any.whl → 0.1.3__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 +5 -1
- pycodex/agent.py +39 -41
- pycodex/cli.py +51 -43
- pycodex/collaboration.py +6 -7
- pycodex/compat.py +99 -0
- pycodex/context.py +87 -87
- pycodex/doctor.py +40 -40
- pycodex/model.py +69 -69
- pycodex/portable.py +33 -33
- pycodex/portable_server.py +22 -21
- pycodex/protocol.py +84 -86
- pycodex/runtime.py +36 -35
- pycodex/runtime_services.py +72 -69
- pycodex/tools/agent_tool_schemas.py +0 -2
- pycodex/tools/apply_patch_tool.py +43 -44
- pycodex/tools/base_tool.py +35 -36
- pycodex/tools/close_agent_tool.py +2 -4
- pycodex/tools/code_mode_manager.py +61 -61
- pycodex/tools/exec_command_tool.py +5 -6
- pycodex/tools/exec_runtime.js +3 -3
- pycodex/tools/exec_tool.py +3 -5
- pycodex/tools/grep_files_tool.py +10 -11
- pycodex/tools/list_dir_tool.py +8 -9
- pycodex/tools/read_file_tool.py +13 -14
- pycodex/tools/request_permissions_tool.py +2 -4
- pycodex/tools/request_user_input_tool.py +13 -14
- pycodex/tools/resume_agent_tool.py +2 -4
- pycodex/tools/send_input_tool.py +8 -9
- pycodex/tools/shell_command_tool.py +5 -6
- pycodex/tools/shell_tool.py +5 -6
- pycodex/tools/spawn_agent_tool.py +4 -5
- pycodex/tools/unified_exec_manager.py +79 -61
- pycodex/tools/update_plan_tool.py +4 -5
- pycodex/tools/view_image_tool.py +4 -5
- pycodex/tools/wait_agent_tool.py +2 -4
- pycodex/tools/wait_tool.py +4 -5
- pycodex/tools/web_search_tool.py +1 -3
- pycodex/tools/write_stdin_tool.py +4 -5
- pycodex/utils/dotenv.py +6 -6
- pycodex/utils/get_env.py +57 -34
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/visualize.py +79 -79
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/METADATA +15 -9
- python_codex-0.1.3.dist-info/RECORD +74 -0
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/WHEEL +1 -1
- responses_server/__init__.py +17 -0
- responses_server/__main__.py +5 -0
- responses_server/app.py +227 -0
- responses_server/config.py +63 -0
- responses_server/payload_processors.py +86 -0
- responses_server/server.py +63 -0
- responses_server/session_store.py +37 -0
- responses_server/stream_router.py +784 -0
- responses_server/tools/__init__.py +4 -0
- responses_server/tools/custom_adapter.py +235 -0
- responses_server/tools/web_search.py +263 -0
- python_codex-0.1.1.dist-info/RECORD +0 -62
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/licenses/LICENSE +0 -0
pycodex/model.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
import asyncio
|
|
4
3
|
import json
|
|
5
4
|
import os
|
|
6
5
|
import urllib.parse
|
|
7
|
-
from collections.abc import Callable
|
|
8
6
|
from dataclasses import dataclass, field, replace
|
|
9
7
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
8
|
+
from typing import Callable
|
|
9
|
+
from .compat import Protocol
|
|
11
10
|
|
|
12
11
|
import requests
|
|
12
|
+
import typing
|
|
13
13
|
|
|
14
14
|
try:
|
|
15
15
|
import tomllib
|
|
@@ -29,38 +29,38 @@ from .utils import build_user_agent, uuid7_string
|
|
|
29
29
|
DEFAULT_CODEX_CONFIG_PATH = Path.home() / ".codex" / "config.toml"
|
|
30
30
|
DEFAULT_ORIGINATOR = "pycodex"
|
|
31
31
|
ModelStreamEventHandler = Callable[[ModelStreamEvent], None]
|
|
32
|
-
NOOP_MODEL_STREAM_EVENT_HANDLER: ModelStreamEventHandler = lambda _event: None
|
|
32
|
+
NOOP_MODEL_STREAM_EVENT_HANDLER: 'ModelStreamEventHandler' = lambda _event: None
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class ModelClient(Protocol):
|
|
36
36
|
async def complete(
|
|
37
37
|
self,
|
|
38
|
-
prompt: Prompt,
|
|
39
|
-
event_handler: ModelStreamEventHandler = NOOP_MODEL_STREAM_EVENT_HANDLER,
|
|
40
|
-
) -> ModelResponse:
|
|
38
|
+
prompt: 'Prompt',
|
|
39
|
+
event_handler: 'ModelStreamEventHandler' = NOOP_MODEL_STREAM_EVENT_HANDLER,
|
|
40
|
+
) -> 'ModelResponse':
|
|
41
41
|
"""Return the next batch of model output items for the current prompt."""
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
@dataclass(frozen=True,
|
|
44
|
+
@dataclass(frozen=True, )
|
|
45
45
|
class ResponsesProviderConfig:
|
|
46
|
-
model: str
|
|
47
|
-
provider_name: str
|
|
48
|
-
base_url: str
|
|
49
|
-
api_key_env: str
|
|
50
|
-
wire_api: str = "responses"
|
|
51
|
-
query_params:
|
|
52
|
-
reasoning_effort: str
|
|
53
|
-
reasoning_summary: str
|
|
54
|
-
verbosity: str
|
|
55
|
-
sandbox_mode: str
|
|
56
|
-
beta_features_header: str
|
|
46
|
+
model: 'str'
|
|
47
|
+
provider_name: 'str'
|
|
48
|
+
base_url: 'str'
|
|
49
|
+
api_key_env: 'str'
|
|
50
|
+
wire_api: 'str' = "responses"
|
|
51
|
+
query_params: 'typing.Dict[str, str]' = field(default_factory=dict)
|
|
52
|
+
reasoning_effort: 'typing.Union[str, None]' = None
|
|
53
|
+
reasoning_summary: 'typing.Union[str, None]' = None
|
|
54
|
+
verbosity: 'typing.Union[str, None]' = None
|
|
55
|
+
sandbox_mode: 'typing.Union[str, None]' = None
|
|
56
|
+
beta_features_header: 'typing.Union[str, None]' = None
|
|
57
57
|
|
|
58
58
|
@classmethod
|
|
59
59
|
def from_codex_config(
|
|
60
60
|
cls,
|
|
61
|
-
config_path: str
|
|
62
|
-
profile: str
|
|
63
|
-
) -> ResponsesProviderConfig:
|
|
61
|
+
config_path: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
|
|
62
|
+
profile: 'typing.Union[str, None]' = None,
|
|
63
|
+
) -> 'ResponsesProviderConfig':
|
|
64
64
|
data = tomllib.loads(Path(config_path).read_text())
|
|
65
65
|
selected = dict(data)
|
|
66
66
|
if profile is not None:
|
|
@@ -86,7 +86,7 @@ class ResponsesProviderConfig:
|
|
|
86
86
|
for key, value in provider.get("query_params", {}).items()
|
|
87
87
|
}
|
|
88
88
|
features = selected.get("features", {})
|
|
89
|
-
beta_features:
|
|
89
|
+
beta_features: 'typing.List[str]' = []
|
|
90
90
|
if isinstance(features, dict) and features.get("guardian_approval") is True:
|
|
91
91
|
beta_features.append("guardian_approval")
|
|
92
92
|
return cls(
|
|
@@ -103,7 +103,7 @@ class ResponsesProviderConfig:
|
|
|
103
103
|
beta_features_header=",".join(beta_features) or None,
|
|
104
104
|
)
|
|
105
105
|
|
|
106
|
-
def api_key(self) -> str:
|
|
106
|
+
def api_key(self) -> 'str':
|
|
107
107
|
value = os.environ.get(self.api_key_env, "")
|
|
108
108
|
if not value:
|
|
109
109
|
raise RuntimeError(
|
|
@@ -113,9 +113,9 @@ class ResponsesProviderConfig:
|
|
|
113
113
|
|
|
114
114
|
def with_overrides(
|
|
115
115
|
self,
|
|
116
|
-
model: str
|
|
117
|
-
reasoning_effort: str
|
|
118
|
-
) -> ResponsesProviderConfig:
|
|
116
|
+
model: 'typing.Union[str, None]' = None,
|
|
117
|
+
reasoning_effort: 'typing.Union[str, None]' = None,
|
|
118
|
+
) -> 'ResponsesProviderConfig':
|
|
119
119
|
return replace(
|
|
120
120
|
self,
|
|
121
121
|
model=self.model if model is None else model,
|
|
@@ -141,13 +141,13 @@ class ResponsesModelClient:
|
|
|
141
141
|
|
|
142
142
|
def __init__(
|
|
143
143
|
self,
|
|
144
|
-
config: ResponsesProviderConfig,
|
|
145
|
-
timeout_seconds: float = 120.0,
|
|
146
|
-
session_id: str
|
|
147
|
-
originator: str = DEFAULT_ORIGINATOR,
|
|
148
|
-
user_agent: str
|
|
149
|
-
openai_subagent: str
|
|
150
|
-
) -> None:
|
|
144
|
+
config: 'ResponsesProviderConfig',
|
|
145
|
+
timeout_seconds: 'float' = 120.0,
|
|
146
|
+
session_id: 'typing.Union[str, None]' = None,
|
|
147
|
+
originator: 'str' = DEFAULT_ORIGINATOR,
|
|
148
|
+
user_agent: 'typing.Union[str, None]' = None,
|
|
149
|
+
openai_subagent: 'typing.Union[str, None]' = None,
|
|
150
|
+
) -> 'None':
|
|
151
151
|
self._config = config
|
|
152
152
|
self.model = config.model
|
|
153
153
|
self._timeout_seconds = timeout_seconds
|
|
@@ -159,22 +159,22 @@ class ResponsesModelClient:
|
|
|
159
159
|
@classmethod
|
|
160
160
|
def from_codex_config(
|
|
161
161
|
cls,
|
|
162
|
-
config_path: str
|
|
163
|
-
profile: str
|
|
164
|
-
timeout_seconds: float = 120.0,
|
|
165
|
-
originator: str = DEFAULT_ORIGINATOR,
|
|
166
|
-
user_agent: str
|
|
167
|
-
) -> ResponsesModelClient:
|
|
162
|
+
config_path: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
|
|
163
|
+
profile: 'typing.Union[str, None]' = None,
|
|
164
|
+
timeout_seconds: 'float' = 120.0,
|
|
165
|
+
originator: 'str' = DEFAULT_ORIGINATOR,
|
|
166
|
+
user_agent: 'typing.Union[str, None]' = None,
|
|
167
|
+
) -> 'ResponsesModelClient':
|
|
168
168
|
config = ResponsesProviderConfig.from_codex_config(config_path, profile)
|
|
169
169
|
return cls(config, timeout_seconds, originator=originator, user_agent=user_agent)
|
|
170
170
|
|
|
171
171
|
def with_overrides(
|
|
172
172
|
self,
|
|
173
|
-
model: str
|
|
174
|
-
reasoning_effort: str
|
|
175
|
-
session_id: str
|
|
176
|
-
openai_subagent: str
|
|
177
|
-
) -> ResponsesModelClient:
|
|
173
|
+
model: 'typing.Union[str, None]' = None,
|
|
174
|
+
reasoning_effort: 'typing.Union[str, None]' = None,
|
|
175
|
+
session_id: 'typing.Union[str, None]' = None,
|
|
176
|
+
openai_subagent: 'typing.Union[str, None]' = None,
|
|
177
|
+
) -> 'ResponsesModelClient':
|
|
178
178
|
return ResponsesModelClient(
|
|
179
179
|
self._config.with_overrides(
|
|
180
180
|
model or self.model,
|
|
@@ -191,35 +191,35 @@ class ResponsesModelClient:
|
|
|
191
191
|
),
|
|
192
192
|
)
|
|
193
193
|
|
|
194
|
-
def responses_url(self) -> str:
|
|
194
|
+
def responses_url(self) -> 'str':
|
|
195
195
|
base_url = self._config.base_url.rstrip("/")
|
|
196
196
|
url = f"{base_url}/responses"
|
|
197
197
|
if self._config.query_params:
|
|
198
198
|
return f"{url}?{urllib.parse.urlencode(self._config.query_params)}"
|
|
199
199
|
return url
|
|
200
200
|
|
|
201
|
-
def models_url(self) -> str:
|
|
201
|
+
def models_url(self) -> 'str':
|
|
202
202
|
base_url = self._config.base_url.rstrip("/")
|
|
203
203
|
url = f"{base_url}/models"
|
|
204
204
|
if self._config.query_params:
|
|
205
205
|
return f"{url}?{urllib.parse.urlencode(self._config.query_params)}"
|
|
206
206
|
return url
|
|
207
207
|
|
|
208
|
-
async def list_models(self) ->
|
|
208
|
+
async def list_models(self) -> 'typing.List[str]':
|
|
209
209
|
return await asyncio.to_thread(self._list_models_sync)
|
|
210
210
|
|
|
211
211
|
async def complete(
|
|
212
212
|
self,
|
|
213
|
-
prompt: Prompt,
|
|
214
|
-
event_handler: ModelStreamEventHandler = NOOP_MODEL_STREAM_EVENT_HANDLER,
|
|
215
|
-
) -> ModelResponse:
|
|
213
|
+
prompt: 'Prompt',
|
|
214
|
+
event_handler: 'ModelStreamEventHandler' = NOOP_MODEL_STREAM_EVENT_HANDLER,
|
|
215
|
+
) -> 'ModelResponse':
|
|
216
216
|
return await asyncio.to_thread(self._complete_sync, prompt, event_handler)
|
|
217
217
|
|
|
218
218
|
def _complete_sync(
|
|
219
219
|
self,
|
|
220
|
-
prompt: Prompt,
|
|
221
|
-
event_handler: ModelStreamEventHandler,
|
|
222
|
-
) -> ModelResponse:
|
|
220
|
+
prompt: 'Prompt',
|
|
221
|
+
event_handler: 'ModelStreamEventHandler',
|
|
222
|
+
) -> 'ModelResponse':
|
|
223
223
|
payload = self._build_payload(prompt)
|
|
224
224
|
body = json.dumps(payload).encode("utf-8")
|
|
225
225
|
url = self.responses_url()
|
|
@@ -262,8 +262,8 @@ class ResponsesModelClient:
|
|
|
262
262
|
except requests.RequestException as exc:
|
|
263
263
|
raise ResponsesApiError(f"responses request failed: {exc}") from exc
|
|
264
264
|
|
|
265
|
-
def _build_payload(self, prompt: Prompt) ->
|
|
266
|
-
payload:
|
|
265
|
+
def _build_payload(self, prompt: 'Prompt') -> 'typing.Dict[str, object]':
|
|
266
|
+
payload: 'typing.Dict[str, object]' = {
|
|
267
267
|
"model": self.model,
|
|
268
268
|
"instructions": prompt.base_instructions or "",
|
|
269
269
|
"input": [item.serialize() for item in prompt.input],
|
|
@@ -276,7 +276,7 @@ class ResponsesModelClient:
|
|
|
276
276
|
"prompt_cache_key": self._session_id,
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
reasoning:
|
|
279
|
+
reasoning: 'typing.Dict[str, str]' = {}
|
|
280
280
|
if self._config.reasoning_effort is not None:
|
|
281
281
|
reasoning["effort"] = self._config.reasoning_effort
|
|
282
282
|
if self._config.reasoning_summary is not None:
|
|
@@ -292,7 +292,7 @@ class ResponsesModelClient:
|
|
|
292
292
|
|
|
293
293
|
return payload
|
|
294
294
|
|
|
295
|
-
def _list_models_sync(self) ->
|
|
295
|
+
def _list_models_sync(self) -> 'typing.List[str]':
|
|
296
296
|
prepared = requests.PreparedRequest()
|
|
297
297
|
prepared.prepare(
|
|
298
298
|
method="GET",
|
|
@@ -330,7 +330,7 @@ class ResponsesModelClient:
|
|
|
330
330
|
data = payload.get("data")
|
|
331
331
|
if not isinstance(data, list):
|
|
332
332
|
raise ResponsesApiError("models response is missing `data` list")
|
|
333
|
-
models:
|
|
333
|
+
models: 'typing.List[str]' = []
|
|
334
334
|
for item in data:
|
|
335
335
|
if not isinstance(item, dict):
|
|
336
336
|
continue
|
|
@@ -339,7 +339,7 @@ class ResponsesModelClient:
|
|
|
339
339
|
models.append(model_id)
|
|
340
340
|
return models
|
|
341
341
|
|
|
342
|
-
def _build_headers(self, prompt: Prompt) ->
|
|
342
|
+
def _build_headers(self, prompt: 'Prompt') -> 'typing.Dict[str, str]':
|
|
343
343
|
headers = {
|
|
344
344
|
"content-type": "application/json",
|
|
345
345
|
"accept": "text/event-stream",
|
|
@@ -360,7 +360,7 @@ class ResponsesModelClient:
|
|
|
360
360
|
)
|
|
361
361
|
return headers
|
|
362
362
|
|
|
363
|
-
def _build_model_list_headers(self) ->
|
|
363
|
+
def _build_model_list_headers(self) -> 'typing.Dict[str, str]':
|
|
364
364
|
headers = {
|
|
365
365
|
"accept": "application/json",
|
|
366
366
|
"authorization": f"Bearer {self._config.api_key()}",
|
|
@@ -376,9 +376,9 @@ class ResponsesModelClient:
|
|
|
376
376
|
def _parse_stream(
|
|
377
377
|
self,
|
|
378
378
|
response,
|
|
379
|
-
event_handler: ModelStreamEventHandler,
|
|
380
|
-
) -> ModelResponse:
|
|
381
|
-
items:
|
|
379
|
+
event_handler: 'ModelStreamEventHandler',
|
|
380
|
+
) -> 'ModelResponse':
|
|
381
|
+
items: 'typing.List[typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem]]' = []
|
|
382
382
|
saw_completed = False
|
|
383
383
|
|
|
384
384
|
for event_name, data in self._iter_sse_events(response):
|
|
@@ -463,8 +463,8 @@ class ResponsesModelClient:
|
|
|
463
463
|
|
|
464
464
|
def _parse_output_item(
|
|
465
465
|
self,
|
|
466
|
-
item:
|
|
467
|
-
) -> AssistantMessage
|
|
466
|
+
item: 'typing.Dict[str, object]',
|
|
467
|
+
) -> 'typing.Union[typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem], None]':
|
|
468
468
|
item_type = item.get("type")
|
|
469
469
|
if item_type == "reasoning":
|
|
470
470
|
return ReasoningItem(payload=dict(item))
|
|
@@ -501,8 +501,8 @@ class ResponsesModelClient:
|
|
|
501
501
|
return None
|
|
502
502
|
|
|
503
503
|
def _iter_sse_events(self, response):
|
|
504
|
-
event_name: str
|
|
505
|
-
data_lines:
|
|
504
|
+
event_name: 'typing.Union[str, None]' = None
|
|
505
|
+
data_lines: 'typing.List[str]' = []
|
|
506
506
|
|
|
507
507
|
for raw_line in response:
|
|
508
508
|
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")
|
|
@@ -525,7 +525,7 @@ class ResponsesModelClient:
|
|
|
525
525
|
yield event_name or "message", "\n".join(data_lines)
|
|
526
526
|
|
|
527
527
|
|
|
528
|
-
def _requests_verify_setting() -> str
|
|
528
|
+
def _requests_verify_setting() -> 'typing.Union[typing.Union[str, bool], None]':
|
|
529
529
|
for env_name in ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE"):
|
|
530
530
|
value = os.environ.get(env_name, "").strip()
|
|
531
531
|
if value:
|
pycodex/portable.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
import hashlib
|
|
4
3
|
import json
|
|
@@ -6,14 +5,15 @@ import os
|
|
|
6
5
|
import shutil
|
|
7
6
|
import tempfile
|
|
8
7
|
import zipfile
|
|
9
|
-
from collections.abc import Callable
|
|
10
8
|
from io import BytesIO
|
|
11
9
|
from pathlib import Path, PurePosixPath
|
|
10
|
+
from typing import Callable
|
|
12
11
|
from urllib.parse import quote, urlparse
|
|
13
12
|
|
|
14
13
|
import requests
|
|
15
14
|
from cryptography.exceptions import InvalidTag
|
|
16
15
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
16
|
+
import typing
|
|
17
17
|
|
|
18
18
|
try:
|
|
19
19
|
import tomllib
|
|
@@ -47,9 +47,9 @@ ProgressHandler = Callable[[str], None]
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def upload_codex_home(
|
|
50
|
-
put_text: str
|
|
51
|
-
event_handler: ProgressHandler
|
|
52
|
-
) -> str:
|
|
50
|
+
put_text: 'typing.Union[str, None]' = None,
|
|
51
|
+
event_handler: 'typing.Union[ProgressHandler, None]' = None,
|
|
52
|
+
) -> 'str':
|
|
53
53
|
source_dir, server = _parse_put_spec(put_text)
|
|
54
54
|
resolved_source_dir = resolve_put_source_dir(source_dir)
|
|
55
55
|
server_address, base_url = resolve_storage_server(server)
|
|
@@ -87,9 +87,9 @@ def upload_codex_home(
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
def bootstrap_called_home(
|
|
90
|
-
call_text: str,
|
|
91
|
-
storage_root: str
|
|
92
|
-
) -> Path:
|
|
90
|
+
call_text: 'str',
|
|
91
|
+
storage_root: 'typing.Union[typing.Union[str, Path], None]' = None,
|
|
92
|
+
) -> 'Path':
|
|
93
93
|
secret, call_id, server_address, base_url = _parse_call_spec(call_text)
|
|
94
94
|
root = resolve_storage_root(storage_root)
|
|
95
95
|
cache_key = hashlib.sha256(call_text.strip().encode("utf-8")).hexdigest()[:16]
|
|
@@ -114,7 +114,7 @@ def bootstrap_called_home(
|
|
|
114
114
|
extracted_home = _resolve_extracted_home(extract_root)
|
|
115
115
|
if home_dir.exists():
|
|
116
116
|
shutil.rmtree(home_dir)
|
|
117
|
-
shutil.
|
|
117
|
+
shutil.copytree(str(extracted_home), str(home_dir))
|
|
118
118
|
metadata_path.write_text(
|
|
119
119
|
json.dumps(
|
|
120
120
|
{
|
|
@@ -128,7 +128,7 @@ def bootstrap_called_home(
|
|
|
128
128
|
return home_dir / DEFAULT_ENTRY_CONFIG
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
def resolve_put_source_dir(source_dir: str
|
|
131
|
+
def resolve_put_source_dir(source_dir: 'typing.Union[typing.Union[str, Path], None]') -> 'Path':
|
|
132
132
|
if source_dir is None or str(source_dir).strip() == "":
|
|
133
133
|
candidate = Path.home() / ".codex"
|
|
134
134
|
else:
|
|
@@ -144,7 +144,7 @@ def resolve_put_source_dir(source_dir: str | Path | None) -> Path:
|
|
|
144
144
|
return resolved
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
def resolve_storage_root(storage_root: str
|
|
147
|
+
def resolve_storage_root(storage_root: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'Path':
|
|
148
148
|
if storage_root is not None:
|
|
149
149
|
return Path(storage_root).expanduser().resolve()
|
|
150
150
|
env_value = os.environ.get(STORAGE_ROOT_ENV, "").strip()
|
|
@@ -153,7 +153,7 @@ def resolve_storage_root(storage_root: str | Path | None = None) -> Path:
|
|
|
153
153
|
return _discover_project_root() / STORAGE_CACHE_DIRNAME
|
|
154
154
|
|
|
155
155
|
|
|
156
|
-
def resolve_storage_server(server: str
|
|
156
|
+
def resolve_storage_server(server: 'typing.Union[str, None]' = None) -> 'typing.Tuple[str, str]':
|
|
157
157
|
raw_value = (server or os.environ.get(STORAGE_SERVER_ENV) or "").strip()
|
|
158
158
|
if not raw_value:
|
|
159
159
|
raw_value = DEFAULT_STORAGE_SERVER
|
|
@@ -167,7 +167,7 @@ def resolve_storage_server(server: str | None = None) -> tuple[str, str]:
|
|
|
167
167
|
return raw_value, f"http://{raw_value}{STORAGE_API_PREFIX}"
|
|
168
168
|
|
|
169
169
|
|
|
170
|
-
def _build_bundle_bytes(root: Path, emit: ProgressHandler) -> bytes:
|
|
170
|
+
def _build_bundle_bytes(root: 'Path', emit: 'ProgressHandler') -> 'bytes':
|
|
171
171
|
files = _collect_upload_files(root)
|
|
172
172
|
emit("[put] mode: whitelist")
|
|
173
173
|
emit(f"[put] packing {len(files)} files")
|
|
@@ -179,8 +179,8 @@ def _build_bundle_bytes(root: Path, emit: ProgressHandler) -> bytes:
|
|
|
179
179
|
return buffer.getvalue()
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
def _collect_upload_files(root: Path) ->
|
|
183
|
-
included:
|
|
182
|
+
def _collect_upload_files(root: 'Path') -> 'typing.List[str]':
|
|
183
|
+
included: 'typing.Set[str]' = set()
|
|
184
184
|
for relative_name in ALLOWED_TOP_LEVEL_FILES:
|
|
185
185
|
candidate = root / relative_name
|
|
186
186
|
if candidate.is_file():
|
|
@@ -195,12 +195,12 @@ def _collect_upload_files(root: Path) -> list[str]:
|
|
|
195
195
|
return sorted(included)
|
|
196
196
|
|
|
197
197
|
|
|
198
|
-
def _collect_config_referenced_files(root: Path) ->
|
|
198
|
+
def _collect_config_referenced_files(root: 'Path') -> 'typing.Set[str]':
|
|
199
199
|
config_path = root / DEFAULT_ENTRY_CONFIG
|
|
200
200
|
if not config_path.is_file():
|
|
201
201
|
return set()
|
|
202
202
|
data = tomllib.loads(config_path.read_text())
|
|
203
|
-
referenced:
|
|
203
|
+
referenced: 'typing.Set[str]' = set()
|
|
204
204
|
candidates = [data]
|
|
205
205
|
profiles = data.get("profiles")
|
|
206
206
|
if isinstance(profiles, dict):
|
|
@@ -217,7 +217,7 @@ def _collect_config_referenced_files(root: Path) -> set[str]:
|
|
|
217
217
|
return referenced
|
|
218
218
|
|
|
219
219
|
|
|
220
|
-
def _normalize_optional_relative_file(root: Path, value: str) -> str
|
|
220
|
+
def _normalize_optional_relative_file(root: 'Path', value: 'str') -> 'typing.Union[str, None]':
|
|
221
221
|
candidate = Path(value)
|
|
222
222
|
if candidate.is_absolute():
|
|
223
223
|
return None
|
|
@@ -231,13 +231,13 @@ def _normalize_optional_relative_file(root: Path, value: str) -> str | None:
|
|
|
231
231
|
return resolved.relative_to(root_resolved).as_posix()
|
|
232
232
|
|
|
233
233
|
|
|
234
|
-
def _encrypt_bundle(bundle_bytes: bytes, secret: str) -> bytes:
|
|
234
|
+
def _encrypt_bundle(bundle_bytes: 'bytes', secret: 'str') -> 'bytes':
|
|
235
235
|
nonce = os.urandom(NONCE_LENGTH)
|
|
236
236
|
ciphertext = AESGCM(_encryption_key(secret)).encrypt(nonce, bundle_bytes, None)
|
|
237
237
|
return ENCRYPTED_BUNDLE_MAGIC + nonce + ciphertext
|
|
238
238
|
|
|
239
239
|
|
|
240
|
-
def _decrypt_bundle(payload: bytes, secret: str) -> bytes:
|
|
240
|
+
def _decrypt_bundle(payload: 'bytes', secret: 'str') -> 'bytes':
|
|
241
241
|
if not payload.startswith(ENCRYPTED_BUNDLE_MAGIC):
|
|
242
242
|
raise RemoteStorageError("stored bundle is not a recognized encrypted payload")
|
|
243
243
|
nonce = payload[len(ENCRYPTED_BUNDLE_MAGIC) : len(ENCRYPTED_BUNDLE_MAGIC) + NONCE_LENGTH]
|
|
@@ -248,19 +248,19 @@ def _decrypt_bundle(payload: bytes, secret: str) -> bytes:
|
|
|
248
248
|
raise RemoteStorageError("call secret is invalid or bundle is corrupted") from exc
|
|
249
249
|
|
|
250
250
|
|
|
251
|
-
def _encryption_key(secret: str) -> bytes:
|
|
251
|
+
def _encryption_key(secret: 'str') -> 'bytes':
|
|
252
252
|
return hashlib.sha256(secret.encode("utf-8")).digest()
|
|
253
253
|
|
|
254
254
|
|
|
255
|
-
def _call_id_from_payload(payload: bytes) -> str:
|
|
255
|
+
def _call_id_from_payload(payload: 'bytes') -> 'str':
|
|
256
256
|
return _base58_encode(hashlib.sha256(payload).digest()[:8])
|
|
257
257
|
|
|
258
258
|
|
|
259
|
-
def _base58_encode(payload: bytes) -> str:
|
|
259
|
+
def _base58_encode(payload: 'bytes') -> 'str':
|
|
260
260
|
number = int.from_bytes(payload, "big")
|
|
261
261
|
if number == 0:
|
|
262
262
|
return TOKEN_BASE58_ALPHABET[0]
|
|
263
|
-
encoded:
|
|
263
|
+
encoded: 'typing.List[str]' = []
|
|
264
264
|
while number:
|
|
265
265
|
number, remainder = divmod(number, 58)
|
|
266
266
|
encoded.append(TOKEN_BASE58_ALPHABET[remainder])
|
|
@@ -269,7 +269,7 @@ def _base58_encode(payload: bytes) -> str:
|
|
|
269
269
|
return prefix + "".join(encoded)
|
|
270
270
|
|
|
271
271
|
|
|
272
|
-
def _parse_put_spec(put_text: str
|
|
272
|
+
def _parse_put_spec(put_text: 'typing.Union[str, None]') -> 'typing.Tuple[typing.Union[str, None], typing.Union[str, None]]':
|
|
273
273
|
raw_value = (put_text or "").strip()
|
|
274
274
|
if not raw_value:
|
|
275
275
|
return None, None
|
|
@@ -288,7 +288,7 @@ def _parse_put_spec(put_text: str | None) -> tuple[str | None, str | None]:
|
|
|
288
288
|
return raw_value, None
|
|
289
289
|
|
|
290
290
|
|
|
291
|
-
def _parse_call_spec(call_text: str) ->
|
|
291
|
+
def _parse_call_spec(call_text: 'str') -> 'typing.Tuple[str, str, str, str]':
|
|
292
292
|
raw_value = call_text.strip()
|
|
293
293
|
if not raw_value or "@" not in raw_value:
|
|
294
294
|
raise RemoteStorageError("call spec must look like <secret>-<call_id>@<host:port>")
|
|
@@ -302,7 +302,7 @@ def _parse_call_spec(call_text: str) -> tuple[str, str, str, str]:
|
|
|
302
302
|
return secret, call_id, server_address, base_url
|
|
303
303
|
|
|
304
304
|
|
|
305
|
-
def _download_encrypted_bundle(base_url: str, call_id: str) -> bytes:
|
|
305
|
+
def _download_encrypted_bundle(base_url: 'str', call_id: 'str') -> 'bytes':
|
|
306
306
|
url = f"{base_url}/call/{quote(call_id, safe='')}"
|
|
307
307
|
try:
|
|
308
308
|
response = requests.get(url, timeout=(5.0, 120.0))
|
|
@@ -319,7 +319,7 @@ def _download_encrypted_bundle(base_url: str, call_id: str) -> bytes:
|
|
|
319
319
|
return payload
|
|
320
320
|
|
|
321
321
|
|
|
322
|
-
def _extract_bundle_bytes(bundle_bytes: bytes, destination: Path) -> None:
|
|
322
|
+
def _extract_bundle_bytes(bundle_bytes: 'bytes', destination: 'Path') -> 'None':
|
|
323
323
|
destination.mkdir(parents=True, exist_ok=True)
|
|
324
324
|
destination_resolved = destination.resolve()
|
|
325
325
|
try:
|
|
@@ -338,7 +338,7 @@ def _extract_bundle_bytes(bundle_bytes: bytes, destination: Path) -> None:
|
|
|
338
338
|
archive.extractall(destination)
|
|
339
339
|
|
|
340
340
|
|
|
341
|
-
def _resolve_extracted_home(extract_root: Path) -> Path:
|
|
341
|
+
def _resolve_extracted_home(extract_root: 'Path') -> 'Path':
|
|
342
342
|
direct_config = extract_root / DEFAULT_ENTRY_CONFIG
|
|
343
343
|
if direct_config.is_file():
|
|
344
344
|
return extract_root
|
|
@@ -348,7 +348,7 @@ def _resolve_extracted_home(extract_root: Path) -> Path:
|
|
|
348
348
|
raise RemoteStorageError("bundle is missing required config file after extraction")
|
|
349
349
|
|
|
350
350
|
|
|
351
|
-
def _load_cached_metadata(metadata_path: Path) ->
|
|
351
|
+
def _load_cached_metadata(metadata_path: 'Path') -> 'typing.Dict[str, object]':
|
|
352
352
|
if not metadata_path.is_file():
|
|
353
353
|
return {}
|
|
354
354
|
try:
|
|
@@ -358,7 +358,7 @@ def _load_cached_metadata(metadata_path: Path) -> dict[str, object]:
|
|
|
358
358
|
return payload if isinstance(payload, dict) else {}
|
|
359
359
|
|
|
360
360
|
|
|
361
|
-
def _check_storage_server(server_address: str, base_url: str) -> None:
|
|
361
|
+
def _check_storage_server(server_address: 'str', base_url: 'str') -> 'None':
|
|
362
362
|
parsed = urlparse(base_url)
|
|
363
363
|
health_url = f"{parsed.scheme}://{parsed.netloc}{HEALTHCHECK_PATH}"
|
|
364
364
|
try:
|
|
@@ -373,7 +373,7 @@ def _check_storage_server(server_address: str, base_url: str) -> None:
|
|
|
373
373
|
)
|
|
374
374
|
|
|
375
375
|
|
|
376
|
-
def _discover_project_root(start: Path
|
|
376
|
+
def _discover_project_root(start: 'typing.Union[Path, None]' = None) -> 'Path':
|
|
377
377
|
current = (start or Path.cwd()).resolve()
|
|
378
378
|
for candidate in (current, *current.parents):
|
|
379
379
|
if (candidate / "pyproject.toml").is_file() and (candidate / "pycodex").is_dir():
|
|
@@ -381,7 +381,7 @@ def _discover_project_root(start: Path | None = None) -> Path:
|
|
|
381
381
|
return current
|
|
382
382
|
|
|
383
383
|
|
|
384
|
-
def _normalize_member_path(value: str, field_name: str) -> str:
|
|
384
|
+
def _normalize_member_path(value: 'str', field_name: 'str') -> 'str':
|
|
385
385
|
path = PurePosixPath(value)
|
|
386
386
|
if not value or path.is_absolute():
|
|
387
387
|
raise RemoteStorageError(f"{field_name} must be relative")
|