python-codex 0.3.3__py3-none-any.whl → 0.4.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.
Files changed (58) 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 +1 -1
  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/{compat.py → utils/compat.py} +2 -0
  32. pycodex/utils/crypto.py +35 -0
  33. pycodex/utils/debug.py +24 -0
  34. pycodex/{doctor.py → utils/doctor.py} +3 -3
  35. pycodex/utils/image_utils.py +3 -2
  36. pycodex/{model_metadata.py → utils/model_metadata.py} +2 -2
  37. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/METADATA +120 -42
  38. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/RECORD +42 -50
  39. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/entry_points.txt +0 -1
  40. responses_server/app.py +28 -15
  41. pycodex/feishu_card.py +0 -793
  42. pycodex/feishu_link.py +0 -440
  43. pycodex/tools/ipython_tool.py +0 -142
  44. workspace_server/__init__.py +0 -39
  45. workspace_server/__main__.py +0 -4
  46. workspace_server/app.py +0 -1906
  47. workspace_server/content_views.LICENSE.txt +0 -470
  48. workspace_server/content_views.css +0 -1
  49. workspace_server/content_views.mjs +0 -25
  50. workspace_server/editor.css +0 -1
  51. workspace_server/editor.mjs +0 -1260
  52. workspace_server/owned_process.py +0 -27
  53. workspace_server/workspace.html +0 -2267
  54. workspace_server/workspace_views.py +0 -533
  55. workspace_server/workspaces.html +0 -550
  56. workspace_server/workspaces.py +0 -632
  57. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/WHEEL +0 -0
  58. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,175 @@
1
+ # The MIT License (MIT)
2
+ #
3
+ # Copyright (c) 2014 Richard Moore
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ """AES encryption subset of pyaes 1.6.1; see README.md for changes."""
24
+
25
+
26
+ class AES256:
27
+ """AES-256 block encryption, used for both directions of GCM."""
28
+
29
+ S = (
30
+ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5,
31
+ 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
32
+ 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0,
33
+ 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
34
+ 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc,
35
+ 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
36
+ 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a,
37
+ 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
38
+ 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0,
39
+ 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
40
+ 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b,
41
+ 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
42
+ 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85,
43
+ 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
44
+ 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5,
45
+ 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
46
+ 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17,
47
+ 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
48
+ 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88,
49
+ 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
50
+ 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c,
51
+ 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
52
+ 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9,
53
+ 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
54
+ 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6,
55
+ 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
56
+ 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e,
57
+ 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
58
+ 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94,
59
+ 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
60
+ 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68,
61
+ 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16,
62
+ )
63
+
64
+ T1 = (
65
+ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554,
66
+ 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a,
67
+ 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b,
68
+ 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b,
69
+ 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f,
70
+ 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f,
71
+ 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5,
72
+ 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f,
73
+ 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb,
74
+ 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497,
75
+ 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed,
76
+ 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a,
77
+ 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594,
78
+ 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3,
79
+ 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504,
80
+ 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d,
81
+ 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739,
82
+ 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395,
83
+ 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883,
84
+ 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76,
85
+ 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4,
86
+ 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b,
87
+ 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0,
88
+ 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818,
89
+ 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651,
90
+ 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85,
91
+ 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12,
92
+ 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9,
93
+ 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7,
94
+ 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a,
95
+ 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8,
96
+ 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a,
97
+ )
98
+
99
+ # The other encryption tables are byte rotations of T1.
100
+ T2 = tuple((value >> 8) | ((value & 0xff) << 24) for value in T1)
101
+ T3 = tuple((value >> 16) | ((value & 0xffff) << 16) for value in T1)
102
+ T4 = tuple((value >> 24) | ((value & 0xffffff) << 8) for value in T1)
103
+ rcon = (0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40)
104
+
105
+ def __init__(self, key):
106
+ if len(key) != 32:
107
+ raise ValueError("AES-256 requires a 32-byte key")
108
+
109
+ self._Ke = [[0] * 4 for _ in range(15)]
110
+ tk = [int.from_bytes(key[i:i + 4], "big") for i in range(0, 32, 4)]
111
+ for i in range(8):
112
+ self._Ke[i // 4][i % 4] = tk[i]
113
+
114
+ # AES-256 key expansion, FIPS 197 section 5.2.
115
+ rconpointer = 0
116
+ t = 8
117
+ while t < 60:
118
+ tt = tk[7]
119
+ tk[0] ^= (
120
+ (self.S[(tt >> 16) & 0xff] << 24)
121
+ ^ (self.S[(tt >> 8) & 0xff] << 16)
122
+ ^ (self.S[tt & 0xff] << 8)
123
+ ^ self.S[(tt >> 24) & 0xff]
124
+ ^ (self.rcon[rconpointer] << 24)
125
+ )
126
+ rconpointer += 1
127
+ for i in range(1, 4):
128
+ tk[i] ^= tk[i - 1]
129
+ tt = tk[3]
130
+ tk[4] ^= (
131
+ self.S[tt & 0xff]
132
+ ^ (self.S[(tt >> 8) & 0xff] << 8)
133
+ ^ (self.S[(tt >> 16) & 0xff] << 16)
134
+ ^ (self.S[(tt >> 24) & 0xff] << 24)
135
+ )
136
+ for i in range(5, 8):
137
+ tk[i] ^= tk[i - 1]
138
+
139
+ j = 0
140
+ while j < 8 and t < 60:
141
+ self._Ke[t // 4][t % 4] = tk[j]
142
+ j += 1
143
+ t += 1
144
+
145
+ def encrypt(self, plaintext):
146
+ """Encrypt one 16-byte block without padding."""
147
+ if len(plaintext) != 16:
148
+ raise ValueError("AES requires a 16-byte block")
149
+
150
+ keys = self._Ke
151
+ sbox, t1, t2, t3, t4 = self.S, self.T1, self.T2, self.T3, self.T4
152
+ t = [
153
+ int.from_bytes(plaintext[4 * i:4 * i + 4], "big") ^ keys[0][i]
154
+ for i in range(4)
155
+ ]
156
+ a = [0] * 4
157
+ for r in range(1, 14):
158
+ for i in range(4):
159
+ a[i] = (
160
+ t1[(t[i] >> 24) & 0xff]
161
+ ^ t2[(t[(i + 1) % 4] >> 16) & 0xff]
162
+ ^ t3[(t[(i + 2) % 4] >> 8) & 0xff]
163
+ ^ t4[t[(i + 3) % 4] & 0xff]
164
+ ^ keys[r][i]
165
+ )
166
+ t = a[:]
167
+
168
+ result = []
169
+ for i in range(4):
170
+ tt = keys[14][i]
171
+ result.append((sbox[(t[i] >> 24) & 0xff] ^ (tt >> 24)) & 0xff)
172
+ result.append((sbox[(t[(i + 1) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff)
173
+ result.append((sbox[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff)
174
+ result.append((sbox[t[(i + 3) % 4] & 0xff] ^ tt) & 0xff)
175
+ return bytes(result)
@@ -0,0 +1,73 @@
1
+ # Copyright (c) 2008, The Chromium Authors. All rights reserved.
2
+ # Adapted from tlslite-ng 0.8.2, tlslite/utils/aesgcm.py (author: Google).
3
+ # GCM derived there from Go's crypto/cipher implementation.
4
+ # See LICENSE for the BSD license and README.md for local changes.
5
+
6
+ """GCM for 12-byte nonces, 16-byte tags and empty associated data."""
7
+
8
+ from .aes import AES256
9
+
10
+
11
+ class AESGCM:
12
+ # Reduction by 1+x+x^2+x^7 for each four-bit polynomial.
13
+ _reduction = (
14
+ 0x0000, 0x1c20, 0x3840, 0x2460, 0x7080, 0x6ca0, 0x48c0, 0x54e0,
15
+ 0xe100, 0xfd20, 0xd940, 0xc560, 0x9180, 0x8da0, 0xa9c0, 0xb5e0,
16
+ )
17
+
18
+ def __init__(self, key):
19
+ self._aes = AES256(key)
20
+ h = int.from_bytes(self._aes.encrypt(b"\0" * 16), "big")
21
+ self._product_table = [0] * 16
22
+ self._product_table[8] = h
23
+ for i in range(2, 16, 2):
24
+ self._product_table[self._reverse_bits(i)] = self._shift(
25
+ self._product_table[self._reverse_bits(i // 2)]
26
+ )
27
+ self._product_table[self._reverse_bits(i + 1)] = (
28
+ self._product_table[self._reverse_bits(i)] ^ h
29
+ )
30
+
31
+ def crypt(self, nonce, data):
32
+ """Apply GCM's counter stream to plaintext or authenticated ciphertext."""
33
+ output = bytearray(len(data))
34
+ for offset in range(0, len(data), 16):
35
+ counter = nonce + (2 + offset // 16).to_bytes(4, "big")
36
+ mask = self._aes.encrypt(counter)
37
+ for i, value in enumerate(data[offset:offset + 16]):
38
+ output[offset + i] = value ^ mask[i]
39
+ return bytes(output)
40
+
41
+ def tag(self, nonce, ciphertext):
42
+ """Authenticate ciphertext with empty associated data."""
43
+ y = 0
44
+ for offset in range(0, len(ciphertext), 16):
45
+ block = ciphertext[offset:offset + 16].ljust(16, b"\0")
46
+ y = self._multiply(y ^ int.from_bytes(block, "big"))
47
+ y = self._multiply(y ^ (len(ciphertext) << 3))
48
+ mask = self._aes.encrypt(nonce + b"\0\0\0\1")
49
+ return (y ^ int.from_bytes(mask, "big")).to_bytes(16, "big")
50
+
51
+ def _multiply(self, y):
52
+ """Multiply by the GCM key in GF(2^128), four bits at a time."""
53
+ result = 0
54
+ for _ in range(0, 128, 4):
55
+ high = result & 0xf
56
+ result >>= 4
57
+ result ^= self._reduction[high] << 112
58
+ result ^= self._product_table[y & 0xf]
59
+ y >>= 4
60
+ return result
61
+
62
+ @staticmethod
63
+ def _reverse_bits(value):
64
+ value = ((value << 2) & 0xc) | ((value >> 2) & 0x3)
65
+ return ((value << 1) & 0xa) | ((value >> 1) & 0x5)
66
+
67
+ @staticmethod
68
+ def _shift(value):
69
+ high = value & 1
70
+ value >>= 1
71
+ if high:
72
+ value ^= 0xe1 << 120
73
+ return value
@@ -0,0 +1,4 @@
1
+ {
2
+ "pyaes-1.6.1/pyaes/aes.py": "5dc0ff7ad948704ae9814fc2720f9b479599092a7a46e1fd9d55b78953a1a43f",
3
+ "tlslite-ng-0.8.2/tlslite/utils/aesgcm.py": "958eb056fe56bcec7b4c398a5af2eccda73ed3c3f7d8ca7866e52bbe415621a3"
4
+ }
@@ -1,3 +1,5 @@
1
+ """Compatibility helpers for supported Python versions."""
2
+
1
3
  import asyncio
2
4
  import functools
3
5
  import shlex
@@ -0,0 +1,35 @@
1
+ """AES-256-GCM using a bundled pure Python implementation."""
2
+
3
+ from hmac import compare_digest
4
+
5
+ from ._aes_gcm.gcm import AESGCM
6
+
7
+
8
+ def _validate(key, nonce, data):
9
+ if len(key) != 32:
10
+ raise ValueError("AES-256-GCM requires a 32-byte key")
11
+ if len(nonce) != 12:
12
+ raise ValueError("AES-GCM requires a 12-byte nonce")
13
+ if len(data) > (1 << 36) - 32:
14
+ raise ValueError("AES-GCM input exceeds the maximum message length")
15
+
16
+
17
+ def aes_gcm_encrypt(key: "bytes", nonce: "bytes", plaintext: "bytes"):
18
+ """Return ciphertext and a 16-byte authentication tag."""
19
+ _validate(key, nonce, plaintext)
20
+ cipher = AESGCM(key)
21
+ ciphertext = cipher.crypt(nonce, plaintext)
22
+ return ciphertext, cipher.tag(nonce, ciphertext)
23
+
24
+
25
+ def aes_gcm_decrypt(
26
+ key: "bytes", nonce: "bytes", ciphertext: "bytes", tag: "bytes"
27
+ ) -> "bytes":
28
+ """Verify the authentication tag before decrypting any ciphertext."""
29
+ _validate(key, nonce, ciphertext)
30
+ if len(tag) != 16:
31
+ raise ValueError("AES-GCM requires a 16-byte authentication tag")
32
+ cipher = AESGCM(key)
33
+ if not compare_digest(tag, cipher.tag(nonce, ciphertext)):
34
+ raise ValueError("AES-GCM authentication failed")
35
+ return cipher.crypt(nonce, ciphertext)
pycodex/utils/debug.py CHANGED
@@ -10,3 +10,27 @@ def get_debug_dir() -> "typing.Union[Path, None]":
10
10
  path = Path(value).expanduser()
11
11
  path.mkdir(parents=True, exist_ok=True)
12
12
  return path
13
+
14
+
15
+ def debug(stop: "bool" = False):
16
+ import socket
17
+
18
+ import debugpy
19
+
20
+ if debugpy.is_client_connected():
21
+ return
22
+ try:
23
+ host = socket.gethostbyname(socket.getfqdn(socket.gethostname()))
24
+ port = 5000
25
+
26
+ debugpy.listen((host, port), in_process_debug_adapter=False)
27
+ print("Waiting for debugger...\a")
28
+ print(f"ip: {host} port:{port}", flush=True)
29
+ debugpy.wait_for_client()
30
+ print("Connected.")
31
+ if stop:
32
+ debugpy.breakpoint()
33
+ except Exception as e:
34
+ import traceback
35
+
36
+ print("\n".join(traceback.format_exception(e)))
@@ -10,9 +10,9 @@ from pathlib import Path
10
10
 
11
11
  import requests
12
12
 
13
- from .model import ResponsesModelClient, ResponsesProviderConfig
14
- from .protocol import AssistantMessage, Prompt, UserMessage
15
- from .utils.dotenv import DOTENV_FILENAME, load_codex_dotenv
13
+ from ..model import ResponsesModelClient, ResponsesProviderConfig
14
+ from ..protocol import AssistantMessage, Prompt, UserMessage
15
+ from .dotenv import DOTENV_FILENAME, load_codex_dotenv
16
16
 
17
17
 
18
18
  @dataclass
@@ -8,6 +8,7 @@ Expected behavior:
8
8
  attached to a model request, matching upstream `PromptImageMode::ResizeToFit`.
9
9
  - Keep the original bytes when the caller asks for `original` detail or the
10
10
  image already fits.
11
+ - Import Pillow only when resizing is requested.
11
12
  """
12
13
 
13
14
  import base64
@@ -16,8 +17,6 @@ import mimetypes
16
17
  import typing
17
18
  from pathlib import Path
18
19
 
19
- from PIL import Image
20
-
21
20
  MAX_DIMENSION = 2048
22
21
 
23
22
  _PRESERVABLE_MIME_TYPES = ("image/png", "image/jpeg", "image/webp")
@@ -48,6 +47,8 @@ def _resize_to_fit(
48
47
  mime_type: "str",
49
48
  image_bytes: "bytes",
50
49
  ) -> "typing.Tuple[str, bytes]":
50
+ from PIL import Image
51
+
51
52
  with Image.open(io.BytesIO(image_bytes)) as image:
52
53
  width, height = image.size
53
54
  if width <= MAX_DIMENSION and height <= MAX_DIMENSION:
@@ -6,9 +6,9 @@ import typing
6
6
  from functools import lru_cache
7
7
  from pathlib import Path
8
8
 
9
- from .protocol import JSONDict
9
+ from ..protocol import JSONDict
10
10
 
11
- DEFAULT_MODELS_PATH = Path(__file__).resolve().parent / "prompts" / "models.json"
11
+ DEFAULT_MODELS_PATH = Path(__file__).resolve().parents[1] / "prompts" / "models.json"
12
12
 
13
13
 
14
14
  @lru_cache(maxsize=1)
@@ -1,20 +1,17 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: A minimal Python extraction of Codex's main agent loop
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.6.2
7
7
  Requires-Dist: dataclasses>=0.8; python_version < '3.7'
8
- Requires-Dist: fastapi<0.84,>=0.83.0; python_version < '3.7'
9
- Requires-Dist: fastapi>=0.83.0; python_version >= '3.7'
10
8
  Requires-Dist: importlib-metadata>=4.8.3; python_version < '3.8'
11
9
  Requires-Dist: loguru>=0.7.3
12
- Requires-Dist: pillow>=8.4.0; python_version < '3.7'
13
- Requires-Dist: pillow>=9.0.0; python_version >= '3.7'
14
10
  Requires-Dist: prompt-toolkit>=3.0.36
15
- Requires-Dist: pycryptodomex>=3.20
16
11
  Requires-Dist: pyyaml>=6.0
17
12
  Requires-Dist: requests>=2.27.1
13
+ Requires-Dist: starlette<0.20,>=0.19.1; python_version < '3.7'
14
+ Requires-Dist: starlette>=0.19.1; python_version >= '3.7'
18
15
  Requires-Dist: tomli<2,>=1.2.3; python_version < '3.11'
19
16
  Requires-Dist: typing-extensions>=4.1.1; python_version < '3.8'
20
17
  Requires-Dist: uvicorn<0.17,>=0.16.0; python_version < '3.7'
@@ -25,6 +22,8 @@ Description-Content-Type: text/markdown
25
22
 
26
23
  English README. Chinese version: `README_ZH.md`
27
24
 
25
+ 0.4.0 core/workspace package split and dependency cleanup: [release notes](docs/RELEASE_0.4.0.md).
26
+
28
27
  0.3.3 workspace viewers and reasoning stream fix: [release notes](docs/RELEASE_0.3.3.md).
29
28
 
30
29
  0.3.2 startup fixes: [release notes](docs/RELEASE_0.3.2.md).
@@ -33,15 +32,33 @@ English README. Chinese version: `README_ZH.md`
33
32
 
34
33
  PyPI distributions:
35
34
 
36
- - Primary package: `python-codex`
37
- - Workspace install alias: `pycodex-ws`
35
+ - Core package: `python-codex`
36
+ - Workspace and integrations: `pycodex-ws`
38
37
 
39
38
  The import path remains `pycodex`; the CLI commands are `pycodex` and
40
39
  `pycodex-ws`.
41
40
 
42
- `pycodex-ws` is a thin metapackage that depends on the exact matching
43
- `python-codex` version. The implementation and console script remain owned by
44
- `python-codex`, so the two distributions never install duplicate modules.
41
+ `python-codex` contains the Agent, runtime, CLI, standard tools, Responses
42
+ compatibility server and portable storage support. `pycodex-ws` depends on the
43
+ exact matching core version and contains the browser workspace, viewer assets,
44
+ Feishu and IPython integrations, including their dependencies. It owns the
45
+ `workspace_server` and `pycodex_ws` modules and the `pycodex-ws` command.
46
+
47
+ Core does not require Pillow. Without it, `view_image` sends the original image
48
+ bytes with `detail="original"`, including when `high` is requested. With Pillow
49
+ installed, the default path retains resizing to a maximum dimension of 2048.
50
+ `pycodex-ws` installs Pillow for image preview validation.
51
+
52
+ Portable encryption uses a bundled pure Python AES-GCM subset of pyaes and
53
+ tlslite-ng, under MIT and BSD licenses. Core does not require a crypto package
54
+ or compiler and ships a `py3-none-any` wheel. Existing `PCX1` bundles and call
55
+ tokens remain compatible; large bundles take longer to encrypt and decrypt.
56
+ See the [source notes](pycodex/utils/_aes_gcm/README.md).
57
+
58
+ The Responses compatibility server and workspace HTTP server use Starlette
59
+ and Uvicorn directly. Neither package requires FastAPI, Pydantic or
60
+ `pydantic-core`. Python 3.6 uses Starlette 0.19.x; newer interpreters resolve
61
+ a compatible Starlette release through its Python version metadata.
45
62
 
46
63
  This repository extracts the core Codex agent loop from upstream Codex
47
64
  (`https://github.com/openai/codex`) into a deliberately small Python version,
@@ -61,16 +78,17 @@ Relevant Rust reference points:
61
78
 
62
79
  ## Quick Start
63
80
 
64
- Install the full package or the workspace-oriented alias:
81
+ Install the core CLI, or install the workspace package to include all integrations:
65
82
 
66
83
  ```bash
67
84
  python3 -m pip install python-codex
68
85
  python3 -m pip install pycodex-ws
69
86
  ```
70
87
 
71
- Use Python 3.6.2 or newer. Both console commands are installed into that
72
- interpreter's scripts directory. On a machine with multiple Python installations,
73
- you can select the same interpreter explicitly for startup:
88
+ The core supports Python 3.6.2 or newer. The workspace package requires Python
89
+ 3.7 or newer because of its Feishu SDK dependency. Commands are installed into
90
+ that interpreter's scripts directory. On a machine with multiple Python
91
+ installations, select the same interpreter explicitly for startup:
74
92
 
75
93
  ```bash
76
94
  python3 -m pycodex
@@ -81,10 +99,14 @@ If `pycodex` is missing from `PATH` or resolves to an older installation, use th
81
99
  module entry points. `python3 -m pip --version` shows which Python environment
82
100
  received the installation.
83
101
 
102
+ For an existing Web installation, use `python3 -m pip install --upgrade pycodex-ws`
103
+ to upgrade the workspace and its matching core together. Installing only
104
+ `python-codex` provides the CLI and core API.
105
+
84
106
  For development from this repository, install dependencies first:
85
107
 
86
108
  ```bash
87
- uv sync
109
+ uv sync --dev
88
110
  ```
89
111
 
90
112
  Try the real entry points:
@@ -94,8 +116,22 @@ uv run pycodex "Reply with exactly OK."
94
116
  uv run pycodex
95
117
  ```
96
118
 
97
- A VS Code frontend is available in [`extensions/vscode`](extensions/vscode/README.md).
98
- It reuses an existing `python-codex>=0.3.1` installation. On first use, missing
119
+ Tests are split by package. `uv run --dev pytest` runs both suites;
120
+ `uv run --dev pytest packages/pycodex-ws/tests` runs workspace and integration
121
+ tests. To check core without installing workspace or its SDKs, use a separate
122
+ environment:
123
+
124
+ ```bash
125
+ UV_PROJECT_ENVIRONMENT=.tmp/core-tests uv sync --group test
126
+ UV_PROJECT_ENVIRONMENT=.tmp/core-tests uv run --group test pytest tests
127
+ ```
128
+
129
+ See [test environments and coverage](tests/TESTS.md). Core tests support Python
130
+ 3.6.2+; workspace tests require Python 3.7+.
131
+
132
+ A VS Code frontend is available in [`packages/pycodex-ws/extensions/vscode`](packages/pycodex-ws/extensions/vscode/README.md).
133
+ It belongs to the workspace frontend and is packaged separately as a VSIX.
134
+ It reuses an existing `pycodex-ws>=0.4.0` installation. On first use, missing
99
135
  or older backends are installed/upgraded with pip in the selected Python
100
136
  environment on the workspace host; compatible versions need no network check.
101
137
  It starts a dedicated `pycodex-ws` workspace on the project's host and opens a
@@ -143,19 +179,35 @@ nailing the core tool-augmented reasoning loop first.
143
179
 
144
180
  ## Layout
145
181
 
182
+ The top level of `pycodex/` contains the core components and package entry points.
183
+ Supporting services live in `tools/`, `portable/` and `utils/`.
184
+
185
+ - `pycodex/agent.py`: inner turn loop
186
+ - `pycodex/runtime.py`: session commands, submission queues and frontend event subscriptions
187
+ - `pycodex/model.py`: model client protocol and Responses API adapter
188
+ - `pycodex/context.py`: configuration, prompt assembly and instruction loading
146
189
  - `pycodex/protocol.py`: minimal conversation item / prompt protocol
147
190
  - `pycodex/events.py`: typed events, plain-text views and stateful presentation
148
- - `pycodex/utils/event_helpers.py`: stateless text, color, summary and result-formatting helpers
149
- - `pycodex/utils/image_utils.py`: image loading, resizing and data-URL preparation
150
- - `pycodex/model.py`: model client protocol and Responses API adapter
151
- - `pycodex/cli.py`: single-turn/interactive entry points, terminal I/O executor and input loop
152
191
  - `pycodex/bootstrap.py`: frontend-independent model, tools, Agent and session assembly
192
+ - `pycodex/cli.py`: single-turn/interactive entry points, terminal I/O executor and input loop
153
193
  - `pycodex/tools/base_tool.py`: `BaseTool`, `ToolRegistry`, `ToolContext`
154
- - `pycodex/tools/`: concrete tool implementations
155
- - `pycodex/agent.py`: inner turn loop
156
- - `pycodex/runtime.py`: session commands, submission queues and frontend event subscriptions
157
- - `extensions/vscode/`: VS Code chat frontend using the workspace HTTP API
158
- - `tests/test_agent.py`: core behavior tests
194
+ - `pycodex/tools/runtime_services.py`: plan, sub-agent, user-input and permissions services
195
+ - `pycodex/tools/`: concrete tools and their execution managers
196
+ - `pycodex/portable/`: Codex home storage client and server, both included in core
197
+ - `pycodex/utils/compat.py`: supported-Python compatibility helpers
198
+ - `pycodex/utils/doctor.py`: configuration and connectivity diagnostics
199
+ - `pycodex/utils/model_metadata.py`: vendored model metadata lookup
200
+ - `pycodex/utils/debug.py`: debug paths and the `pycodex.debug()` implementation
201
+ - `pycodex/utils/event_helpers.py`: stateless text, color, summary and result-formatting helpers
202
+ - `pycodex/utils/image_utils.py`: image loading, resizing and data-URL preparation
203
+ - `pycodex/prompts/`: vendored prompts and model metadata
204
+ - `packages/pycodex-ws/workspace_server/`: Web board, API and file/diff viewers
205
+ - `packages/pycodex-ws/pycodex_ws/`: Feishu, IPython and workspace assembly
206
+ - `packages/pycodex-ws/extensions/vscode/`: workspace's VS Code frontend, built separately as a VSIX
207
+ - `tests/`: core tests
208
+ - `packages/pycodex-ws/tests/`: workspace and cross-frontend integration tests
209
+
210
+ See [package layout and import paths](docs/RUNTIME.md#package-layout).
159
211
 
160
212
  ## Current Alignment Status
161
213
 
@@ -266,7 +318,7 @@ Current behavior:
266
318
  - with an argv prompt or piped stdin, run a single turn
267
319
  - `pycodex-ws` starts the standalone browser workspace manager and serves each
268
320
  workspace with a board pane and a pycodex session pane
269
- - the left pane is the board, containing a live HTML Canvas tab and closable read-only file/diff tabs that
321
+ - the left pane is the board, containing an optional live HTML Canvas tab and closable read-only file/diff tabs that
270
322
  scroll together in one toolbar; a mouse wheel scrolls overflowing tab strips
271
323
  horizontally.
272
324
  Click Markdown file links such as `[source](src/app.py:42)` or
@@ -285,6 +337,14 @@ Current behavior:
285
337
  hover titles retain the complete paths and revisions. Text,
286
338
  Markdown, HTML, common raster images and text diffs are supported. Monaco
287
339
  provides syntax highlighting, find, folding and character-level diffs.
340
+ Right-click code and choose **Insert into chat**, or select text and use the
341
+ toolbar button, to insert context at the current chat draft's cursor.
342
+ Context appears as a separate block among your words. Click it to edit or
343
+ remove it; Backspace and Delete also remove a block as one element.
344
+ Context identifies the full view label, including both revisions of a diff.
345
+ Selections include only the selected text and range; otherwise it records the
346
+ cursor location. Diff context also identifies the selected side.
347
+ The draft stays editable until you send it.
288
348
  Its code and styles load only when a code or diff view opens, so Canvas,
289
349
  Markdown and HTML previews do not load the editor. Viewer assets use gzip
290
350
  when supported and revalidate cached content with ETags.
@@ -353,28 +413,37 @@ Current behavior:
353
413
  non-persisted follow-up session and submits the file contents as the next
354
414
  user instruction; this is intended for side-effect follow-ups such as
355
415
  Feishu notifications
356
- - `/link <feishu-email|open_id|chat_id>` attaches the current interactive
357
- session to a Feishu card; multiple sessions in the same pycodex process share
416
+ - `/link <feishu-email|open_id|chat_id>` in a workspace session attaches it
417
+ to a Feishu card; multiple sessions in the same workspace process share
358
418
  one Feishu long-connection listener and route card actions by message id
359
419
  - Feishu user OAuth stores its refresh token only in
360
420
  `~/.codex/.feishu_refresh_token` (mode `0600`). Run
361
- `env -u VIRTUAL_ENV uv run python tools/feishu_oauth.py` from this repository
362
- to authorize; app credentials can stay in `~/.codex/.env`. Each refresh reads
421
+ `python3 -m pycodex_ws.feishu_oauth` after installing `pycodex-ws` to authorize.
422
+ From this repository, use `env -u VIRTUAL_ENV uv run --dev python -m pycodex_ws.feishu_oauth`.
423
+ App credentials can stay in `~/.codex/.env`. Each refresh reads
363
424
  the latest token from the dedicated file and saves its replacement there.
364
425
  `FEISHU_REFRESH_TOKEN` in `.env` or the environment is no longer used and
365
426
  can be removed. Refresh is triggered by API calls, with no idle keepalive.
366
427
  - `pycodex-ws --workspace-config workspaces.json` serves workspace boards from
367
428
  one process. The JSON file can be either a list or
368
- `{"workspaces": [...]}`; each entry uses `board` and `work_dir`, with optional
369
- display/URL `id`, and is exposed at `/w/<id>/`. Internally the resolved
370
- `board` path is the workspace identity, so adding the same board path twice is
371
- rejected even with a different name. Relative `board` and `work_dir` paths
429
+ `{"workspaces": [...]}`; each entry uses `state_path` and `work_dir`, with optional
430
+ display/URL `id` and `enable_board` (default `true`), and is exposed at `/w/<id>/`.
431
+ `state_path` is a filename prefix: `/data/demo` uses
432
+ `/data/demo.pycodex-ws.json` for the session index,
433
+ `/data/demo.pycodex-views.json` for file views and `/data/demo.html` for Canvas.
434
+ Suffixes are appended, so prefixes containing dots remain distinct.
435
+ The resolved `state_path` is the workspace identity; duplicates are rejected
436
+ regardless of name or Canvas setting. Relative `state_path` and `work_dir` paths
372
437
  resolve from the config file directory. The root path `/` shows a workspaces
373
- management page with `add_workspace(name=None, dir="./", board=None)` and
374
- `delete(name)` controls; omitted names become `workspace-1`, `workspace-2`,
375
- etc. If `board` is omitted when adding a workspace, pycodex assigns a random
376
- writable `/tmp/pcws-*.html` board path. Add/delete actions and later
377
- session-state saves refresh the JSON file. Board HTML can reference local
438
+ management page with create/delete controls; omitted names become `workspace-1`,
439
+ `workspace-2`, etc. Omitted `state_path` when creating a workspace generates a
440
+ writable `/tmp/pcws-*` prefix. `enable_board: false` hides Canvas and disables its
441
+ routes, polling and context injection; sessions and file views still persist.
442
+ Existing `board: "demo.html"` and `board: false` configurations load with their
443
+ original state paths and are saved in the new format. See the
444
+ [workspace configuration example](packages/pycodex-ws/README.md).
445
+ Add/delete actions and later session-state saves refresh the JSON file.
446
+ Board HTML can reference local
378
447
  images beside the board (including nested paths) with relative URLs; only
379
448
  `image/*` files contained by the board directory are served.
380
449
  Assistant Markdown supports KaTeX formulas with `$...$`, `$$...$$`,
@@ -466,6 +535,13 @@ pycodex doctor --json
466
535
  `Portable Mode` is the quickest way to bring your usual `pycodex` setup into a
467
536
  fresh machine, container, or debug image.
468
537
 
538
+ The storage client and server are both included in `python-codex`. To start a
539
+ local storage service, run this in a separate terminal:
540
+
541
+ ```bash
542
+ python -m pycodex.portable.server --root .tmp/pycodex_storage --host 127.0.0.1 --port 5577
543
+ ```
544
+
469
545
  Use it like this:
470
546
 
471
547
  ```bash
@@ -659,7 +735,9 @@ CLI, Web and Feishu all use `AgentRuntime.submit_input`: not just `/model`
659
735
  and `/resume`, but every session command, steer/queue admission, interactive
660
736
  questions and permissions, state notifications and shutdown. Frontends only
661
737
  adapt input and render events. Web no longer imports or runs the CLI shell.
662
- IPython is the deliberate exception: `ipython_agent()` still returns a bare Agent.
738
+ IPython is the deliberate exception: `pycodex_ws.ipython.ipython_agent()` returns
739
+ a bare Agent. Workspace assembly uses `pycodex_ws.build_runtime()` to add
740
+ `/link` and `/unlink`; `pycodex.bootstrap.build_runtime()` provides core commands.
663
741
 
664
742
  ```python
665
743
  from pycodex.bootstrap import build_agent, build_model, build_runtime