python-codex 0.3.3__py3-none-any.whl → 0.4.1__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.
Files changed (61) hide show
  1. pycodex/__init__.py +3 -27
  2. pycodex/bootstrap.py +5 -45
  3. pycodex/cli.py +1 -14
  4. pycodex/context.py +1 -1
  5. pycodex/events.py +1 -1
  6. pycodex/model.py +2 -2
  7. pycodex/portable/__init__.py +45 -0
  8. pycodex/{portable.py → portable/client.py} +4 -7
  9. pycodex/{portable_server.py → portable/server.py} +3 -3
  10. pycodex/protocol.py +1 -1
  11. pycodex/runtime.py +2 -2
  12. pycodex/tools/base_tool.py +1 -1
  13. pycodex/tools/close_agent_tool.py +1 -1
  14. pycodex/tools/code_mode_manager.py +1 -1
  15. pycodex/tools/request_permissions_tool.py +1 -1
  16. pycodex/tools/resume_agent_tool.py +1 -1
  17. pycodex/{runtime_services.py → tools/runtime_services.py} +5 -5
  18. pycodex/tools/send_input_tool.py +1 -1
  19. pycodex/tools/spawn_agent_tool.py +1 -1
  20. pycodex/tools/unified_exec_manager.py +1 -1
  21. pycodex/tools/update_plan_tool.py +1 -1
  22. pycodex/tools/view_image_tool.py +5 -0
  23. pycodex/tools/wait_agent_tool.py +1 -1
  24. pycodex/utils/__init__.py +0 -4
  25. pycodex/utils/_aes_gcm/LICENSE +56 -0
  26. pycodex/utils/_aes_gcm/README.md +61 -0
  27. pycodex/utils/_aes_gcm/__init__.py +1 -0
  28. pycodex/utils/_aes_gcm/aes.py +175 -0
  29. pycodex/utils/_aes_gcm/gcm.py +73 -0
  30. pycodex/utils/_aes_gcm/source-sha256.json +4 -0
  31. pycodex/utils/async_bridge.py +3 -4
  32. pycodex/{compat.py → utils/compat.py} +2 -0
  33. pycodex/utils/crypto.py +35 -0
  34. pycodex/utils/debug.py +24 -0
  35. pycodex/{doctor.py → utils/doctor.py} +3 -3
  36. pycodex/utils/image_utils.py +3 -2
  37. pycodex/{model_metadata.py → utils/model_metadata.py} +2 -2
  38. pycodex/utils/session_persist.py +28 -0
  39. {python_codex-0.3.3.dist-info → python_codex-0.4.1.dist-info}/METADATA +124 -42
  40. python_codex-0.4.1.dist-info/RECORD +90 -0
  41. {python_codex-0.3.3.dist-info → python_codex-0.4.1.dist-info}/entry_points.txt +0 -1
  42. responses_server/app.py +28 -15
  43. pycodex/feishu_card.py +0 -793
  44. pycodex/feishu_link.py +0 -440
  45. pycodex/tools/ipython_tool.py +0 -142
  46. python_codex-0.3.3.dist-info/RECORD +0 -98
  47. workspace_server/__init__.py +0 -39
  48. workspace_server/__main__.py +0 -4
  49. workspace_server/app.py +0 -1906
  50. workspace_server/content_views.LICENSE.txt +0 -470
  51. workspace_server/content_views.css +0 -1
  52. workspace_server/content_views.mjs +0 -25
  53. workspace_server/editor.css +0 -1
  54. workspace_server/editor.mjs +0 -1260
  55. workspace_server/owned_process.py +0 -27
  56. workspace_server/workspace.html +0 -2267
  57. workspace_server/workspace_views.py +0 -533
  58. workspace_server/workspaces.html +0 -550
  59. workspace_server/workspaces.py +0 -632
  60. {python_codex-0.3.3.dist-info → python_codex-0.4.1.dist-info}/WHEEL +0 -0
  61. {python_codex-0.3.3.dist-info → python_codex-0.4.1.dist-info}/licenses/LICENSE +0 -0
pycodex/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .compat import patch_asyncio
1
+ from .utils.compat import patch_asyncio
2
2
 
3
3
  patch_asyncio()
4
4
 
@@ -27,7 +27,7 @@ from .protocol import (
27
27
  UserMessage,
28
28
  )
29
29
  from .runtime import AgentRuntime
30
- from .runtime_services import (
30
+ from .tools.runtime_services import (
31
31
  PlanStore,
32
32
  RequestPermissionsManager,
33
33
  RequestUserInputManager,
@@ -64,31 +64,7 @@ from .tools import (
64
64
  WebSearchTool,
65
65
  WriteStdinTool,
66
66
  )
67
-
68
-
69
- def debug(stop: "bool" = False):
70
-
71
- import socket
72
-
73
- import debugpy
74
-
75
- if debugpy.is_client_connected():
76
- return
77
- try:
78
- host = socket.gethostbyname(socket.getfqdn(socket.gethostname()))
79
- port = 5000
80
-
81
- debugpy.listen((host, port), in_process_debug_adapter=False)
82
- print("Waiting for debugger...\a")
83
- print(f"ip: {host} port:{port}", flush=True)
84
- debugpy.wait_for_client()
85
- print("Connected.")
86
- if stop:
87
- debugpy.breakpoint()
88
- except Exception as e:
89
- import traceback
90
-
91
- print("\n".join(traceback.format_exception(e)))
67
+ from .utils.debug import debug
92
68
 
93
69
 
94
70
  __all__ = [
pycodex/bootstrap.py CHANGED
@@ -13,7 +13,10 @@ from .model import (
13
13
  ResponsesProviderConfig,
14
14
  )
15
15
  from .runtime import AgentRuntime
16
- from .runtime_services import AgentRuntimeEnvironment, create_agent_runtime_environment
16
+ from .tools.runtime_services import (
17
+ AgentRuntimeEnvironment,
18
+ create_agent_runtime_environment,
19
+ )
17
20
  from .utils import get_debug_dir, load_codex_dotenv, uuid7_string
18
21
  from .utils.session_persist import rollout_path_for_session
19
22
 
@@ -371,47 +374,4 @@ def build_model(
371
374
 
372
375
 
373
376
  def build_runtime(agent: "Agent") -> "AgentRuntime":
374
- runtime = AgentRuntime(agent)
375
- register_connection_commands(runtime)
376
- return runtime
377
-
378
-
379
- def register_connection_commands(runtime):
380
- link = None
381
-
382
- async def unlink(argument):
383
- nonlocal link
384
- if argument:
385
- raise ValueError("Usage: /unlink")
386
- if link is None:
387
- return {"kind": "connection", "lines": ["No Feishu card is linked."]}
388
- link.detach()
389
- link = None
390
- return {"kind": "connection", "lines": ["Unlinked Feishu card."]}
391
-
392
- async def connect(target):
393
- nonlocal link
394
- if not target:
395
- raise ValueError("Usage: /link <feishu-email|open_id|chat_id>")
396
- if link is not None:
397
- raise RuntimeError("A Feishu card is already linked. Use /unlink first.")
398
- from .feishu_link import PycodexRuntimeLink
399
-
400
- link = await PycodexRuntimeLink(runtime, target).start_async()
401
- return {
402
- "kind": "connection",
403
- "lines": [
404
- "Linked Feishu card: session_key={0} message_id={1}".format(
405
- link.session_key,
406
- link.message_id or "-",
407
- ),
408
- ],
409
- }
410
-
411
- async def close_link():
412
- if link is not None:
413
- await unlink("")
414
-
415
- runtime.register_command("link", connect)
416
- runtime.register_command("unlink", unlink)
417
- runtime.add_close_handler(close_link)
377
+ return AgentRuntime(agent)
pycodex/cli.py CHANGED
@@ -354,23 +354,10 @@ class CliSessionView:
354
354
  self.prompter.close()
355
355
 
356
356
 
357
- def ipython_agent(config_path=DEFAULT_CODEX_CONFIG_PATH):
358
- from loguru import logger
359
-
360
- from .tools.ipython_tool import attach_ipython_tool
361
-
362
- logger.remove()
363
- logger.add(sys.stderr, level="INFO")
364
- model = build_model(config_path)
365
- agent = build_agent(client=model, config_path=config_path)
366
- attach_ipython_tool(agent)
367
- return agent
368
-
369
-
370
357
  def main(argv=None):
371
358
  raw_args = list(argv) if argv is not None else sys.argv[1:]
372
359
  if raw_args and raw_args[0] == "doctor":
373
- from .doctor import build_doctor_parser, run_doctor_cli
360
+ from .utils.doctor import build_doctor_parser, run_doctor_cli
374
361
 
375
362
  parser = build_doctor_parser()
376
363
  args = parser.parse_args(raw_args[1:])
pycodex/context.py CHANGED
@@ -11,7 +11,7 @@ try:
11
11
  except ModuleNotFoundError: # pragma: no cover - Python 3.10 path
12
12
  import tomli as tomllib
13
13
 
14
- from .model_metadata import model_metadata
14
+ from .utils.model_metadata import model_metadata
15
15
  from .protocol import ContextMessage, ConversationItem, JSONDict, Prompt, ToolSpec
16
16
  from .utils.get_env import (
17
17
  get_sandbox_tag,
pycodex/events.py CHANGED
@@ -73,7 +73,7 @@ class EventDisplay:
73
73
 
74
74
  def begin_turn(self, event):
75
75
  self.finish_stream()
76
- self.set_status(event.kind)
76
+ self.set_status("understood")
77
77
  submission_id = event.submission_id or event.turn_id
78
78
  for prompts in (self.inserted_steer_prompts, self.queued_steer_prompts):
79
79
  for prompt in prompts.pop(submission_id, []):
pycodex/model.py CHANGED
@@ -12,7 +12,7 @@ from typing import Callable
12
12
 
13
13
  import requests
14
14
 
15
- from .compat import Protocol
15
+ from .utils.compat import Protocol
16
16
 
17
17
  try:
18
18
  import tomllib
@@ -26,7 +26,7 @@ from .events import (
26
26
  TokenCountEvent,
27
27
  ToolCalledEvent,
28
28
  )
29
- from .model_metadata import model_metadata
29
+ from .utils.model_metadata import model_metadata
30
30
  from .protocol import (
31
31
  AssistantMessage,
32
32
  JSONDict,
@@ -0,0 +1,45 @@
1
+ """Portable Codex home storage client API."""
2
+
3
+ from .client import (
4
+ ALLOWED_TOP_LEVEL_DIRS,
5
+ ALLOWED_TOP_LEVEL_FILES,
6
+ DEFAULT_ENTRY_CONFIG,
7
+ DEFAULT_STORAGE_SERVER,
8
+ ENCRYPTED_BUNDLE_MAGIC,
9
+ HEALTHCHECK_PATH,
10
+ NONCE_LENGTH,
11
+ STORAGE_API_PREFIX,
12
+ STORAGE_CACHE_DIRNAME,
13
+ STORAGE_ROOT_ENV,
14
+ STORAGE_SERVER_ENV,
15
+ TOKEN_BASE58_ALPHABET,
16
+ ProgressHandler,
17
+ RemoteStorageError,
18
+ bootstrap_called_home,
19
+ resolve_put_source_dir,
20
+ resolve_storage_root,
21
+ resolve_storage_server,
22
+ upload_codex_home,
23
+ )
24
+
25
+ __all__ = [
26
+ "ALLOWED_TOP_LEVEL_DIRS",
27
+ "ALLOWED_TOP_LEVEL_FILES",
28
+ "DEFAULT_ENTRY_CONFIG",
29
+ "DEFAULT_STORAGE_SERVER",
30
+ "ENCRYPTED_BUNDLE_MAGIC",
31
+ "HEALTHCHECK_PATH",
32
+ "NONCE_LENGTH",
33
+ "STORAGE_API_PREFIX",
34
+ "STORAGE_CACHE_DIRNAME",
35
+ "STORAGE_ROOT_ENV",
36
+ "STORAGE_SERVER_ENV",
37
+ "TOKEN_BASE58_ALPHABET",
38
+ "ProgressHandler",
39
+ "RemoteStorageError",
40
+ "bootstrap_called_home",
41
+ "resolve_put_source_dir",
42
+ "resolve_storage_root",
43
+ "resolve_storage_server",
44
+ "upload_codex_home",
45
+ ]
@@ -11,7 +11,8 @@ from typing import Callable
11
11
  from urllib.parse import quote, urlparse
12
12
 
13
13
  import requests
14
- from Cryptodome.Cipher import AES
14
+
15
+ from ..utils.crypto import aes_gcm_decrypt, aes_gcm_encrypt
15
16
 
16
17
  try:
17
18
  import tomllib
@@ -240,8 +241,7 @@ def _normalize_optional_relative_file(
240
241
 
241
242
  def _encrypt_bundle(bundle_bytes: "bytes", secret: "str") -> "bytes":
242
243
  nonce = os.urandom(NONCE_LENGTH)
243
- cipher = AES.new(_encryption_key(secret), AES.MODE_GCM, nonce=nonce)
244
- ciphertext, tag = cipher.encrypt_and_digest(bundle_bytes)
244
+ ciphertext, tag = aes_gcm_encrypt(_encryption_key(secret), nonce, bundle_bytes)
245
245
  return ENCRYPTED_BUNDLE_MAGIC + nonce + ciphertext + tag
246
246
 
247
247
 
@@ -257,10 +257,7 @@ def _decrypt_bundle(payload: "bytes", secret: "str") -> "bytes":
257
257
  ciphertext = encrypted[:-16]
258
258
  tag = encrypted[-16:]
259
259
  try:
260
- cipher = AES.new(_encryption_key(secret), AES.MODE_GCM, nonce=nonce)
261
- plaintext = cipher.decrypt(ciphertext)
262
- cipher.verify(tag)
263
- return plaintext
260
+ return aes_gcm_decrypt(_encryption_key(secret), nonce, ciphertext, tag)
264
261
  except ValueError as exc:
265
262
  raise RemoteStorageError(
266
263
  "call secret is invalid or bundle is corrupted"
@@ -7,8 +7,8 @@ from http.server import BaseHTTPRequestHandler
7
7
  from pathlib import Path
8
8
  from urllib.parse import unquote, urlparse
9
9
 
10
- from .compat import ThreadingHTTPServer
11
- from .portable import (
10
+ from ..utils.compat import ThreadingHTTPServer
11
+ from .client import (
12
12
  DEFAULT_STORAGE_SERVER,
13
13
  HEALTHCHECK_PATH,
14
14
  STORAGE_API_PREFIX,
@@ -166,7 +166,7 @@ class CodexStorageServer:
166
166
 
167
167
  def build_parser() -> "argparse.ArgumentParser":
168
168
  parser = argparse.ArgumentParser(
169
- prog="python -m pycodex.portable_server",
169
+ prog="python -m pycodex.portable.server",
170
170
  description="Run a pycodex remote storage service for --put/--call testing.",
171
171
  )
172
172
  parser.add_argument(
pycodex/protocol.py CHANGED
@@ -19,7 +19,7 @@ from copy import deepcopy
19
19
  from dataclasses import dataclass, field
20
20
  from typing import Any
21
21
 
22
- from .compat import Literal, TypeAlias
22
+ from .utils.compat import Literal, TypeAlias
23
23
 
24
24
  JSONValue: "TypeAlias" = Any
25
25
  JSONDict: "TypeAlias" = typing.Dict[str, Any]
pycodex/runtime.py CHANGED
@@ -4,7 +4,7 @@ from collections import deque
4
4
  from dataclasses import dataclass, field, replace
5
5
 
6
6
  from .agent import BASE_EVENT_HANDLER, Agent, TurnInterrupted
7
- from .compat import Literal
7
+ from .utils.compat import Literal
8
8
  from .events import (
9
9
  AssistantDeltaEvent,
10
10
  AutoCompactCompletedEvent,
@@ -307,7 +307,7 @@ class AgentRuntime:
307
307
  if not self.agent.history:
308
308
  return {"kind": "compact_empty"}
309
309
  result = await self.agent.compact()
310
- self.publish_state("history")
310
+ self.publish_state("compacted")
311
311
  return {
312
312
  "kind": "compacted",
313
313
  "original_item_count": result.original_item_count,
@@ -27,8 +27,8 @@ from ..protocol import (
27
27
  ToolSpec,
28
28
  UserMessage,
29
29
  )
30
- from ..runtime_services import AgentRuntimeEnvironment
31
30
  from ..utils import get_debug_dir
31
+ from .runtime_services import AgentRuntimeEnvironment
32
32
 
33
33
  if typing.TYPE_CHECKING:
34
34
  from ..agent import Agent
@@ -9,9 +9,9 @@ Expected behavior:
9
9
  """
10
10
 
11
11
  from ..protocol import JSONDict, JSONValue
12
- from ..runtime_services import SubAgentManager
13
12
  from .agent_tool_schemas import AGENT_STATUS_SCHEMA
14
13
  from .base_tool import BaseTool, ToolContext
14
+ from .runtime_services import SubAgentManager
15
15
 
16
16
  CLOSE_AGENT_OUTPUT_SCHEMA = {
17
17
  "type": "object",
@@ -21,7 +21,7 @@ from pathlib import Path
21
21
 
22
22
  from loguru import logger
23
23
 
24
- from ..compat import is_ascii, stream_writer_is_closing
24
+ from ..utils.compat import is_ascii, stream_writer_is_closing
25
25
  from ..protocol import JSONDict, JSONValue, ToolCall
26
26
  from ..utils.truncation import DEFAULT_MAX_OUTPUT_TOKENS
27
27
  from .base_tool import StructuredToolOutput, ToolContext, ToolRegistry
@@ -10,8 +10,8 @@ Expected behavior:
10
10
  """
11
11
 
12
12
  from ..protocol import JSONDict, JSONValue
13
- from ..runtime_services import RequestPermissionsManager
14
13
  from .base_tool import BaseTool, ToolContext
14
+ from .runtime_services import RequestPermissionsManager
15
15
 
16
16
  NETWORK_PERMISSIONS_SCHEMA = {
17
17
  "type": "object",
@@ -9,9 +9,9 @@ Expected behavior:
9
9
  """
10
10
 
11
11
  from ..protocol import JSONDict, JSONValue
12
- from ..runtime_services import SubAgentManager
13
12
  from .agent_tool_schemas import AGENT_STATUS_SCHEMA
14
13
  from .base_tool import BaseTool, ToolContext
14
+ from .runtime_services import SubAgentManager
15
15
 
16
16
  RESUME_AGENT_OUTPUT_SCHEMA = {
17
17
  "type": "object",
@@ -5,8 +5,7 @@ import typing
5
5
  from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, Awaitable, Callable
7
7
 
8
- from .compat import Literal
9
- from .events import (
8
+ from ..events import (
10
9
  CompactCompletedEvent,
11
10
  CompactFailedEvent,
12
11
  CompactStartedEvent,
@@ -16,11 +15,12 @@ from .events import (
16
15
  TurnInterruptedEvent,
17
16
  TurnStartedEvent,
18
17
  )
19
- from .protocol import ConversationItem, ToolCall, ToolResult
20
- from .utils import uuid7_string
18
+ from ..protocol import ConversationItem, ToolCall, ToolResult
19
+ from ..utils import uuid7_string
20
+ from ..utils.compat import Literal
21
21
 
22
22
  if TYPE_CHECKING:
23
- from .runtime import AgentRuntime
23
+ from ..runtime import AgentRuntime
24
24
 
25
25
  PlanStatus = Literal["pending", "in_progress", "completed"]
26
26
  AgentStatus = typing.Union[
@@ -12,9 +12,9 @@ Expected behavior:
12
12
  import typing
13
13
 
14
14
  from ..protocol import JSONDict, JSONValue
15
- from ..runtime_services import SubAgentManager
16
15
  from .agent_tool_schemas import COLLAB_INPUT_ITEMS_SCHEMA
17
16
  from .base_tool import BaseTool, ToolContext
17
+ from .runtime_services import SubAgentManager
18
18
 
19
19
  SEND_INPUT_OUTPUT_SCHEMA = {
20
20
  "type": "object",
@@ -12,9 +12,9 @@ Expected behavior:
12
12
  import typing
13
13
 
14
14
  from ..protocol import JSONDict, JSONValue
15
- from ..runtime_services import SubAgentManager
16
15
  from .agent_tool_schemas import COLLAB_INPUT_ITEMS_SCHEMA
17
16
  from .base_tool import BaseTool, ToolContext
17
+ from .runtime_services import SubAgentManager
18
18
 
19
19
  SPAWN_AGENT_OUTPUT_SCHEMA = {
20
20
  "type": "object",
@@ -22,7 +22,7 @@ from pathlib import Path
22
22
 
23
23
  from loguru import logger
24
24
 
25
- from ..compat import shlex_join, stream_writer_is_closing
25
+ from ..utils.compat import shlex_join, stream_writer_is_closing
26
26
  from ..utils.truncation import (
27
27
  DEFAULT_MAX_OUTPUT_TOKENS,
28
28
  approx_token_count,
@@ -13,8 +13,8 @@ Expected behavior:
13
13
  import typing
14
14
 
15
15
  from ..protocol import JSONDict, JSONValue
16
- from ..runtime_services import PlanItem, PlanStore
17
16
  from .base_tool import BaseTool, ToolContext
17
+ from .runtime_services import PlanItem, PlanStore
18
18
 
19
19
  VALID_PLAN_STATUSES = {"pending", "in_progress", "completed"}
20
20
 
@@ -8,6 +8,7 @@ Expected behavior:
8
8
  into the next model request.
9
9
  - Resize the image down to `MAX_DIMENSION` unless the caller asks for
10
10
  `original` detail, matching upstream `PromptImageMode::ResizeToFit`.
11
+ - Use `original` detail when Pillow is unavailable.
11
12
  - Accept the documented `path` argument plus optional `detail: "high" |
12
13
  "original"` hint.
13
14
  - Return both the JSON object result and the structured `input_image` content
@@ -15,6 +16,7 @@ Expected behavior:
15
16
  """
16
17
 
17
18
  import typing
19
+ from importlib.util import find_spec
18
20
  from pathlib import Path
19
21
 
20
22
  from ..protocol import JSONDict, JSONValue
@@ -95,6 +97,9 @@ class ViewImageTool(BaseTool):
95
97
  if not path.is_file():
96
98
  return f"Error: image path `{path}` is not a file."
97
99
 
100
+ if detail == "high" and find_spec("PIL") is None:
101
+ detail = "original"
102
+
98
103
  try:
99
104
  image_url = load_image_data_url(path, detail != "original")
100
105
  except ImageProcessingError as exc:
@@ -13,9 +13,9 @@ import json
13
13
  import typing
14
14
 
15
15
  from ..protocol import JSONDict, JSONValue, UserMessage
16
- from ..runtime_services import SubAgentManager
17
16
  from .agent_tool_schemas import AGENT_STATUS_SCHEMA
18
17
  from .base_tool import BaseTool, ToolContext
18
+ from .runtime_services import SubAgentManager
19
19
 
20
20
  DEFAULT_WAIT_AGENT_TIMEOUT_MS = 30_000
21
21
  MIN_WAIT_AGENT_TIMEOUT_MS = 10_000
pycodex/utils/__init__.py CHANGED
@@ -1,13 +1,10 @@
1
- from .compactor import DEFAULT_COMPACT_PROMPT, SUMMARY_PREFIX, compact
2
1
  from .debug import get_debug_dir
3
2
  from .dotenv import DOTENV_FILENAME, load_codex_dotenv, parse_dotenv, parse_dotenv_value
4
3
  from .get_env import build_user_agent, get_shell_name, get_timezone_name
5
4
  from .random_ids import uuid7_string
6
5
 
7
6
  __all__ = [
8
- "DEFAULT_COMPACT_PROMPT",
9
7
  "DOTENV_FILENAME",
10
- "SUMMARY_PREFIX",
11
8
  "build_user_agent",
12
9
  "get_debug_dir",
13
10
  "get_shell_name",
@@ -15,6 +12,5 @@ __all__ = [
15
12
  "load_codex_dotenv",
16
13
  "parse_dotenv",
17
14
  "parse_dotenv_value",
18
- "compact",
19
15
  "uuid7_string",
20
16
  ]
@@ -0,0 +1,56 @@
1
+ aes.py is adapted from pyaes 1.6.1 under the following license:
2
+
3
+ The MIT License (MIT)
4
+
5
+ Copyright (c) 2014 Richard Moore
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
24
+
25
+ ---
26
+
27
+ gcm.py is adapted from Google-authored tlslite/utils/aesgcm.py in tlslite-ng
28
+ 0.8.2. Its LICENSE provides the following terms for code written by Google:
29
+
30
+ Copyright (c) 2008, The Chromium Authors
31
+ All rights reserved.
32
+
33
+ Redistribution and use in source and binary forms, with or without
34
+ modification, are permitted provided that the following conditions are met:
35
+
36
+ * Redistributions of source code must retain the above copyright notice, this
37
+ list of conditions and the following disclaimer.
38
+
39
+ * Redistributions in binary form must reproduce the above copyright notice,
40
+ this list of conditions and the following disclaimer in the documentation
41
+ and/or other materials provided with the distribution.
42
+
43
+ * Neither the name of the Google Inc. nor the names of its contributors may
44
+ be used to endorse or promote products derived from this software without
45
+ specific prior written permission.
46
+
47
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
48
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
49
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
50
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
51
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
52
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
53
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
54
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
55
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
56
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,61 @@
1
+ # Bundled pure Python AES-GCM
2
+
3
+ Portable encryption uses this subset through `pycodex.utils.crypto`. It needs
4
+ only Python's standard library, with no compiler, shared library, optional
5
+ crypto package or platform-specific build. Core ships a `py3-none-any` wheel.
6
+
7
+ The format remains AES-256-GCM with a 32-byte key, 12-byte nonce, 16-byte tag and
8
+ empty associated data. The existing `PCX1` layout and SHA-256 key derivation are
9
+ unchanged, so previously issued call tokens continue to work.
10
+
11
+ ## Sources and licenses
12
+
13
+ - `aes.py`: the AES encryption primitive from
14
+ [pyaes 1.6.1](https://pypi.org/project/pyaes/1.6.1/), by Richard Moore, under MIT.
15
+ Upstream repository: https://github.com/ricmoo/pyaes.
16
+ Source archive SHA-256:
17
+ `02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f`.
18
+ - `gcm.py`: adapted from the Google-authored `tlslite/utils/aesgcm.py` in
19
+ [tlslite-ng 0.8.2](https://pypi.org/project/tlslite-ng/0.8.2/), under the BSD
20
+ terms specified for Google's code in its LICENSE. That implementation derives
21
+ GCM multiplication from Go's `crypto/cipher`.
22
+ Upstream repository: https://github.com/tlsfuzzer/tlslite-ng.
23
+
24
+ `LICENSE` retains the applicable notices. `source-sha256.json` identifies the
25
+ original input files, before the local adaptations below.
26
+
27
+ ## Local scope
28
+
29
+ - AES supports only the 256-bit key and 128-bit block used by portable.
30
+ Decryption tables, the inverse AES primitive, other modes, padding and Python 2
31
+ compatibility code are removed. GCM uses the AES encryption primitive in both
32
+ directions.
33
+ - The AES S-box and T1 entries are unchanged. T2/T3/T4 are byte rotations of T1;
34
+ their values were checked against all upstream entries. These tables are
35
+ immutable class data. Key schedules and GCM product tables belong to each call.
36
+ - GCM retains the upstream four-bit multiplication and reduction tables. Its
37
+ fixed nonce, tag and empty-associated-data contract removes generic TLS
38
+ helpers. Counter processing is local and does not allocate a full keystream.
39
+ - `crypto.py` validates lengths and the GCM message-size bound. Decryption checks
40
+ the tag with `hmac.compare_digest` before decrypting any ciphertext.
41
+
42
+ Pure Python execution is deliberately slower than native crypto, especially
43
+ for multi-megabyte bundles. These table-based Python primitives do not provide
44
+ constant-time AES/GHASH execution; constant-time tag comparison does not change
45
+ that property.
46
+
47
+ ## Verification
48
+
49
+ ```bash
50
+ uv sync --dev
51
+ uv run --dev pytest tests/test_crypto.py tests/test_portable.py
52
+ ```
53
+
54
+ Tests cover NIST AES-256-GCM vectors, comparison with PyCryptodome, empty and
55
+ partial blocks, a 1 MiB payload, altered key/nonce/ciphertext/tag, input length
56
+ and counter bounds, authentication before decryption, and existing `PCX1`
57
+ bundles. PyCryptodome is a development-only reference dependency.
58
+
59
+ The publish workflow builds ordinary Python wheels and source distributions.
60
+ `tests/smoke_core_wheel.py` checks a clean core installation without Crypto or
61
+ Cryptodome, including portable round-trip and CLI entry points.
@@ -0,0 +1 @@
1
+ """Vendored pure Python AES-GCM subset; see README.md and LICENSE."""