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
@@ -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
+ }
@@ -5,10 +5,9 @@ import inspect
5
5
  def run_async(awaitable):
6
6
  """Run an awaitable from synchronous code and return its real result.
7
7
 
8
- This helper is intended for sync call sites (for example, IPython cells)
9
- that need to call async agent APIs without managing the event loop
10
- themselves. It returns the underlying result directly and raises on
11
- failure instead of swallowing exceptions.
8
+ This helper lets synchronous callers use async agent APIs without
9
+ managing the event loop themselves. It returns the underlying result
10
+ directly and propagates exceptions.
12
11
  """
13
12
  try:
14
13
  running_loop = asyncio.get_running_loop()
@@ -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)
@@ -3,6 +3,7 @@ import mmap
3
3
  import os
4
4
  import re
5
5
  import typing
6
+ from collections import deque
6
7
  from contextlib import closing
7
8
  from datetime import datetime
8
9
  from pathlib import Path
@@ -323,6 +324,33 @@ def conversation_history_to_turns(
323
324
  return tuple(turns)
324
325
 
325
326
 
327
+ def load_display_history(
328
+ rollout_path: "typing.Union[str, Path]",
329
+ limit: "int" = 80,
330
+ ) -> "typing.Tuple[typing.Union[typing.Tuple[str, str], None], ...]":
331
+ """Read visible turns across checkpoints; None marks a compaction."""
332
+ path = Path(rollout_path).expanduser().resolve()
333
+ turns = deque(maxlen=limit)
334
+ for entry in _iter_rollout_entries(path):
335
+ item_type = entry.get("type")
336
+ payload = entry.get("payload")
337
+ if not isinstance(payload, dict):
338
+ continue
339
+ if item_type == "compacted":
340
+ turns.append(None)
341
+ elif item_type == "event_msg" and payload.get("type") == "user_message":
342
+ turns.append((str(payload.get("message", "")), ""))
343
+ elif (
344
+ item_type == "response_item"
345
+ and payload.get("type") == "message"
346
+ and payload.get("role") == "assistant"
347
+ ):
348
+ if not turns or turns[-1] is None:
349
+ turns.append(("", ""))
350
+ turns[-1] = (turns[-1][0], _extract_response_message_text(payload))
351
+ return tuple(turns)
352
+
353
+
326
354
  def _drop_unmatched_tool_calls(
327
355
  history: "typing.List[ConversationItem]",
328
356
  ) -> "typing.List[ConversationItem]":