relay-codex 0.0.0-stage → 0.4.1

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.
@@ -0,0 +1,297 @@
1
+ """Private Unix-socket credential authority. Tokens never enter the pool database."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import secrets
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ import base64
13
+ import hashlib
14
+
15
+ from pool import file_lock, Pool
16
+ from settings import atomic_json
17
+ from processes import spawn
18
+
19
+ LEGACY_CLIENT_REVISION = (
20
+ "b3b3a2c9e0da6e29ce54d2e1b4b747b4372b31d4eb193c0626eee6a86421c3f7"
21
+ )
22
+ PROTOCOL_VERSION = 1
23
+ SOURCE_REVISION = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
24
+
25
+
26
+ def paths(state):
27
+ # macOS Unix socket paths have a short length limit.
28
+ import hashlib
29
+
30
+ name = hashlib.sha256(str(state.resolve()).encode()).hexdigest()[:20]
31
+ root = Path("/private/tmp") / f"codex-accounts-{os.getuid()}-{name}"
32
+ root.mkdir(mode=0o700, exist_ok=True)
33
+ if root.is_symlink() or root.stat().st_uid != os.getuid():
34
+ raise RuntimeError("Unsafe local socket directory")
35
+ root.chmod(0o700)
36
+ return root / "broker.sock", state / "broker.json"
37
+
38
+
39
+ async def exchange(state, request, timeout=60):
40
+ socket, meta = paths(state)
41
+ data = json.loads(meta.read_text())
42
+
43
+ async def call():
44
+ reader, writer = await asyncio.open_unix_connection(
45
+ str(socket), limit=4 * 1024 * 1024
46
+ )
47
+ try:
48
+ writer.write(
49
+ json.dumps({"token": data["token"], **request}).encode() + b"\n"
50
+ )
51
+ await writer.drain()
52
+ response = json.loads(await reader.readline())
53
+ if not response.get("ok"):
54
+ raise RuntimeError("Credential operation unavailable")
55
+ return response.get("result")
56
+ finally:
57
+ writer.close()
58
+ await writer.wait_closed()
59
+
60
+ return await asyncio.wait_for(call(), timeout)
61
+
62
+
63
+ async def ensure(state, binary):
64
+ async with file_lock(state / "broker-start.lock"):
65
+ try:
66
+ running = await exchange(state, {"op": "ping"}, 3)
67
+ if running.get("protocol", 1) == PROTOCOL_VERSION:
68
+ return
69
+ await exchange(state, {"op": "stop"}, 3)
70
+ for _ in range(50):
71
+ if not paths(state)[1].exists():
72
+ break
73
+ await asyncio.sleep(0.1)
74
+ except (OSError, ValueError, RuntimeError, asyncio.TimeoutError):
75
+ pass
76
+ spawn(
77
+ [
78
+ sys.executable,
79
+ str(Path(__file__).with_name("router.py")),
80
+ "_broker",
81
+ "--binary",
82
+ binary,
83
+ ],
84
+ env={
85
+ **os.environ,
86
+ "CODEX_ACCOUNTS_STATE": str(state),
87
+ "RELAY_STATE": str(state),
88
+ },
89
+ stdin=subprocess.DEVNULL,
90
+ stdout=subprocess.DEVNULL,
91
+ stderr=subprocess.DEVNULL,
92
+ start_new_session=True,
93
+ )
94
+ deadline = time.monotonic() + 15
95
+ while time.monotonic() < deadline:
96
+ try:
97
+ await exchange(state, {"op": "ping"}, 2)
98
+ return
99
+ except (OSError, ValueError, RuntimeError, asyncio.TimeoutError):
100
+ await asyncio.sleep(0.1)
101
+ raise RuntimeError("Local credential coordinator did not start")
102
+
103
+
104
+ class ClientAccount:
105
+ def __init__(self, label, home, binary, pool):
106
+ self.label, self.home, self.binary, self.pool = label, home, binary, pool
107
+
108
+ async def call(self, op, **params):
109
+ from router import RouterError
110
+
111
+ try:
112
+ await ensure(self.pool.state, self.binary)
113
+ return await exchange(
114
+ self.pool.state, {"op": op, "label": self.label, **params}
115
+ )
116
+ except (OSError, ValueError, RuntimeError, asyncio.TimeoutError):
117
+ raise RouterError(
118
+ "Account coordinator could not complete this operation. Check doctor or reauthenticate this account."
119
+ ) from None
120
+
121
+ async def credentials(self, refresh=False):
122
+ return await self.call("credentials", refresh=refresh)
123
+
124
+ async def usage(self, force=False):
125
+ return await self.call(
126
+ "usage", force=force, after=time.time() if force else None
127
+ )
128
+
129
+ async def network(self):
130
+ return await self.call("network")
131
+
132
+ async def close(self):
133
+ pass
134
+
135
+
136
+ async def serve(state, binary):
137
+ from router import Account, account_home, labels, read_keychain
138
+
139
+ socket, meta = paths(state)
140
+ async with file_lock(state / "broker-owner.lock"):
141
+ socket.unlink(missing_ok=True)
142
+ token = secrets.token_urlsafe(32)
143
+ atomic_json(meta, {"token": token, "pid": os.getpid()})
144
+ pool = Pool(state)
145
+ accounts = {}
146
+ locks = {}
147
+ stamps = {}
148
+ refreshed = {}
149
+ last_used = time.monotonic()
150
+ network = {"at": 0, "value": None}
151
+ stop = asyncio.Event()
152
+
153
+ async def handle(reader, writer):
154
+ nonlocal last_used
155
+ response = {"ok": False}
156
+ try:
157
+ raw = await asyncio.wait_for(reader.readline(), 5)
158
+ request = json.loads(raw)
159
+ if not secrets.compare_digest(str(request.get("token", "")), token):
160
+ return
161
+ last_used = time.monotonic()
162
+ op = request.get("op")
163
+ if op == "ping":
164
+ response = {
165
+ "ok": True,
166
+ "result": {
167
+ "pid": os.getpid(),
168
+ "revision": LEGACY_CLIENT_REVISION,
169
+ "protocol": PROTOCOL_VERSION,
170
+ "build": SOURCE_REVISION,
171
+ },
172
+ }
173
+ elif op == "stop":
174
+ response = {"ok": True, "result": True}
175
+ stop.set()
176
+ elif op == "network":
177
+ if time.monotonic() - network["at"] > 30:
178
+ try:
179
+ _, connection = await asyncio.wait_for(
180
+ asyncio.open_connection("auth.openai.com", 443), 3
181
+ )
182
+ connection.close()
183
+ await connection.wait_closed()
184
+ network["value"] = True
185
+ except (OSError, asyncio.TimeoutError):
186
+ network["value"] = False
187
+ network["at"] = time.monotonic()
188
+ response = {"ok": True, "result": network["value"]}
189
+ else:
190
+ label = request.get("label", "")
191
+ if label not in labels(state):
192
+ raise ValueError()
193
+ if pool.options(label).get("maintenance") and op != "invalidate":
194
+ raise ValueError()
195
+ lock = locks.setdefault(label, asyncio.Lock())
196
+ async with lock:
197
+ # Enrollment writes a new generation marker and invalidates
198
+ # this authority before any cached manager is reused.
199
+ marker = account_home(state, label) / "enrolled"
200
+ stamp = marker.stat().st_mtime_ns
201
+ if label in accounts and stamps.get(label) != stamp:
202
+ await accounts.pop(label).close()
203
+ account = accounts.setdefault(
204
+ label,
205
+ Account(label, account_home(state, label), binary, pool),
206
+ )
207
+ stamps[label] = stamp
208
+ async with file_lock(account.home / "credential.lock"):
209
+ if op == "credentials":
210
+ value = None
211
+ with contextlib.suppress(Exception):
212
+ candidate = await asyncio.to_thread(
213
+ read_keychain, account.home
214
+ )
215
+ part = candidate["accessToken"].split(".")[1]
216
+ expires = json.loads(
217
+ base64.urlsafe_b64decode(
218
+ part + "=" * (-len(part) % 4)
219
+ )
220
+ ).get("exp", 0)
221
+ if expires > time.time() + 120 and (
222
+ not request.get("refresh")
223
+ or time.monotonic() - refreshed.get(label, 0)
224
+ < 30
225
+ ):
226
+ value = candidate
227
+ if value is None:
228
+ await account.start()
229
+ result = await account.rpc.call(
230
+ "account/read",
231
+ {"refreshToken": bool(request.get("refresh"))},
232
+ )
233
+ if not result.get("account"):
234
+ raise ValueError()
235
+ value = await asyncio.to_thread(
236
+ read_keychain, account.home
237
+ )
238
+ if request.get("refresh"):
239
+ refreshed[label] = time.monotonic()
240
+ elif op == "usage":
241
+ value = (
242
+ pool.cached(label, not_before=request.get("after"))
243
+ if request.get("force") and request.get("after")
244
+ else pool.cached(label)
245
+ if not request.get("force")
246
+ else None
247
+ )
248
+ if value is None:
249
+ await account.start()
250
+ value = await account.rpc.call(
251
+ "account/rateLimits/read"
252
+ )
253
+ pool.save(label, value)
254
+ elif op == "invalidate":
255
+ await account.close()
256
+ accounts.pop(label, None)
257
+ value = True
258
+ else:
259
+ raise ValueError()
260
+ response = {"ok": True, "result": value}
261
+ except (Exception, asyncio.CancelledError):
262
+ response = {"ok": False}
263
+ finally:
264
+ with contextlib.suppress(Exception):
265
+ writer.write(json.dumps(response).encode() + b"\n")
266
+ await writer.drain()
267
+ writer.close()
268
+ with contextlib.suppress(Exception):
269
+ await writer.wait_closed()
270
+
271
+ server = await asyncio.start_unix_server(
272
+ handle, str(socket), limit=4 * 1024 * 1024
273
+ )
274
+ socket.chmod(0o600)
275
+ try:
276
+ while not stop.is_set():
277
+ try:
278
+ await asyncio.wait_for(stop.wait(), 30)
279
+ except asyncio.TimeoutError:
280
+ pass
281
+ import settings
282
+
283
+ if (
284
+ time.monotonic() - last_used
285
+ > settings.load(state)["broker_idle_seconds"]
286
+ and not pool.detail()
287
+ ):
288
+ break
289
+ finally:
290
+ server.close()
291
+ await server.wait_closed()
292
+ await asyncio.gather(
293
+ *(a.close() for a in accounts.values()), return_exceptions=True
294
+ )
295
+ pool.close()
296
+ socket.unlink(missing_ok=True)
297
+ meta.unlink(missing_ok=True)