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.
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/assets/relay.svg +15 -0
- package/bin/relay.cjs +148 -0
- package/docs/commands.md +92 -0
- package/docs/publishing.md +56 -0
- package/docs/troubleshooting.md +37 -0
- package/package.json +47 -4
- package/python/Menu.swift +70 -0
- package/python/broker.py +297 -0
- package/python/commands.py +701 -0
- package/python/compatibility.py +225 -0
- package/python/desktop.py +52 -0
- package/python/launch.py +117 -0
- package/python/metadata.py +82 -0
- package/python/notifications.py +36 -0
- package/python/pool.py +579 -0
- package/python/probe_native.py +91 -0
- package/python/processes.py +12 -0
- package/python/requirements.lock +6 -0
- package/python/requirements.txt +2 -0
- package/python/router.py +2053 -0
- package/python/runtime.py +97 -0
- package/python/service.py +663 -0
- package/python/settings.py +109 -0
- package/tests/fake_native.py +238 -0
- package/tests/test_capacity.py +217 -0
- package/tests/test_controller.py +519 -0
- package/tests/test_pool.py +446 -0
- package/tests/test_relay.py +173 -0
- package/tests/test_router.py +314 -0
- package/tests/test_services.py +473 -0
package/python/pool.py
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
"""Cross-process coordination. Contains usage metadata, never tokens or prompts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import asyncio
|
|
5
|
+
import contextlib
|
|
6
|
+
import fcntl
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sqlite3
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
import subprocess
|
|
14
|
+
import functools
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextlib.asynccontextmanager
|
|
18
|
+
async def file_lock(path, *, shared=False):
|
|
19
|
+
"""Nonblocking flock with cancellable waiting, never block the event loop."""
|
|
20
|
+
fd = os.open(path, os.O_CREAT | os.O_RDWR, 0o600)
|
|
21
|
+
try:
|
|
22
|
+
while True:
|
|
23
|
+
try:
|
|
24
|
+
fcntl.flock(
|
|
25
|
+
fd, (fcntl.LOCK_SH if shared else fcntl.LOCK_EX) | fcntl.LOCK_NB
|
|
26
|
+
)
|
|
27
|
+
break
|
|
28
|
+
except BlockingIOError:
|
|
29
|
+
await asyncio.sleep(0.05)
|
|
30
|
+
yield
|
|
31
|
+
finally:
|
|
32
|
+
os.close(fd)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def buckets(snapshot, model=None):
|
|
36
|
+
values = snapshot.get("rateLimitsByLimitId")
|
|
37
|
+
if not values:
|
|
38
|
+
return [snapshot.get("rateLimits") or {}]
|
|
39
|
+
return [
|
|
40
|
+
v
|
|
41
|
+
for k, v in values.items()
|
|
42
|
+
if k == "codex" or (model and v.get("normalModelSlug") == model)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def blocked(snapshot, model=None):
|
|
47
|
+
return snapshot.get("ordinaryUsageAllowed") is False or any(
|
|
48
|
+
b.get("rateLimitReachedType") or b.get("spendControlReached")
|
|
49
|
+
for b in buckets(snapshot, model)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def reset_time(snapshot, model=None, now=None):
|
|
54
|
+
"""For simultaneous exhausted windows, all must reset: take the latest.
|
|
55
|
+
|
|
56
|
+
A timestamp only schedules a fresh read; it never establishes recovery.
|
|
57
|
+
"""
|
|
58
|
+
now = time.time() if now is None else now
|
|
59
|
+
windows = [
|
|
60
|
+
b.get(k) or {}
|
|
61
|
+
for b in buckets(snapshot, model)
|
|
62
|
+
for k in ("primary", "secondary")
|
|
63
|
+
]
|
|
64
|
+
exhausted = [w for w in windows if w.get("usedPercent", 0) >= 100]
|
|
65
|
+
dates = [w.get("resetsAt") for w in exhausted]
|
|
66
|
+
if dates and all(isinstance(d, (int, float)) and d > now for d in dates):
|
|
67
|
+
return max(dates) + 2 # Small grace for server clock/propagation.
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Pool:
|
|
72
|
+
def __init__(self, state: Path, *, clock=time.time):
|
|
73
|
+
self.state, self.clock = state, clock
|
|
74
|
+
self.session = uuid.uuid4().hex
|
|
75
|
+
self.path = state / "pool.sqlite"
|
|
76
|
+
with self.db() as db:
|
|
77
|
+
db.executescript("""
|
|
78
|
+
CREATE TABLE IF NOT EXISTS usage(label TEXT PRIMARY KEY, checked REAL, snapshot TEXT);
|
|
79
|
+
CREATE TABLE IF NOT EXISTS blocks(label TEXT, scope TEXT, until REAL, reason TEXT,
|
|
80
|
+
PRIMARY KEY(label,scope));
|
|
81
|
+
CREATE TABLE IF NOT EXISTS sessions(id TEXT PRIMARY KEY,pid INTEGER,label TEXT);
|
|
82
|
+
CREATE TABLE IF NOT EXISTS threads(thread TEXT PRIMARY KEY,owner TEXT,pid INTEGER);
|
|
83
|
+
CREATE TABLE IF NOT EXISTS account_options(label TEXT PRIMARY KEY, options TEXT NOT NULL);
|
|
84
|
+
CREATE TABLE IF NOT EXISTS session_details(id TEXT PRIMARY KEY, birth TEXT, seen REAL, details TEXT NOT NULL);
|
|
85
|
+
CREATE TABLE IF NOT EXISTS reservations(owner TEXT PRIMARY KEY,label TEXT,until REAL);
|
|
86
|
+
CREATE TABLE IF NOT EXISTS intents(owner TEXT,thread TEXT,details TEXT,PRIMARY KEY(owner,thread));
|
|
87
|
+
CREATE TABLE IF NOT EXISTS events(id INTEGER PRIMARY KEY AUTOINCREMENT,at REAL,owner TEXT,kind TEXT,details TEXT);
|
|
88
|
+
CREATE TABLE IF NOT EXISTS history(id INTEGER PRIMARY KEY AUTOINCREMENT,at REAL,label TEXT,snapshot TEXT);
|
|
89
|
+
CREATE TABLE IF NOT EXISTS commands(id INTEGER PRIMARY KEY AUTOINCREMENT,owner TEXT,op TEXT,details TEXT,at REAL,status TEXT);
|
|
90
|
+
CREATE TABLE IF NOT EXISTS signals(name TEXT PRIMARY KEY,revision INTEGER);
|
|
91
|
+
""")
|
|
92
|
+
self.path.chmod(0o600)
|
|
93
|
+
|
|
94
|
+
@contextlib.contextmanager
|
|
95
|
+
def db(self):
|
|
96
|
+
db = sqlite3.connect(self.path, timeout=5)
|
|
97
|
+
try:
|
|
98
|
+
with db:
|
|
99
|
+
yield db
|
|
100
|
+
finally:
|
|
101
|
+
db.close()
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def alive(pid):
|
|
105
|
+
try:
|
|
106
|
+
os.kill(pid, 0)
|
|
107
|
+
return True
|
|
108
|
+
except ProcessLookupError:
|
|
109
|
+
return False
|
|
110
|
+
except PermissionError:
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
def prune(self, db):
|
|
114
|
+
for table, key in [("sessions", "id"), ("threads", "thread")]:
|
|
115
|
+
for ident, pid in db.execute(f"SELECT {key},pid FROM {table}").fetchall():
|
|
116
|
+
row = db.execute(
|
|
117
|
+
"SELECT birth FROM session_details WHERE id=?",
|
|
118
|
+
(
|
|
119
|
+
ident
|
|
120
|
+
if table == "sessions"
|
|
121
|
+
else (
|
|
122
|
+
db.execute(
|
|
123
|
+
"SELECT owner FROM threads WHERE thread=?", (ident,)
|
|
124
|
+
).fetchone()
|
|
125
|
+
or [""]
|
|
126
|
+
)[0],
|
|
127
|
+
),
|
|
128
|
+
).fetchone()
|
|
129
|
+
birth_matches = not row or not row[0] or row[0] == process_birth(pid)
|
|
130
|
+
if not self.alive(pid) or not birth_matches:
|
|
131
|
+
db.execute(f"DELETE FROM {table} WHERE {key}=?", (ident,))
|
|
132
|
+
db.execute(
|
|
133
|
+
"DELETE FROM reservations WHERE until<? OR owner NOT IN (SELECT id FROM sessions)",
|
|
134
|
+
(self.clock(),),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def assign(self, label):
|
|
138
|
+
with self.db() as db:
|
|
139
|
+
self.prune(db)
|
|
140
|
+
db.execute(
|
|
141
|
+
"INSERT OR REPLACE INTO sessions VALUES(?,?,?)",
|
|
142
|
+
(self.session, os.getpid(), label),
|
|
143
|
+
)
|
|
144
|
+
db.execute("DELETE FROM reservations WHERE owner=?", (self.session,))
|
|
145
|
+
self.touch(account=label)
|
|
146
|
+
|
|
147
|
+
def loads(self):
|
|
148
|
+
with self.db() as db:
|
|
149
|
+
self.prune(db)
|
|
150
|
+
rows = dict(
|
|
151
|
+
db.execute(
|
|
152
|
+
"SELECT label,count(*) FROM sessions WHERE id<>? GROUP BY label",
|
|
153
|
+
(self.session,),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
for label, count in db.execute(
|
|
157
|
+
"SELECT label,count(*) FROM reservations WHERE owner<>? GROUP BY label",
|
|
158
|
+
(self.session,),
|
|
159
|
+
):
|
|
160
|
+
rows[label] = rows.get(label, 0) + count
|
|
161
|
+
# Public loads still include this session for status and compatibility.
|
|
162
|
+
own = db.execute(
|
|
163
|
+
"SELECT label FROM sessions WHERE id=?", (self.session,)
|
|
164
|
+
).fetchone()
|
|
165
|
+
if own:
|
|
166
|
+
rows[own[0]] = rows.get(own[0], 0) + 1
|
|
167
|
+
return rows
|
|
168
|
+
|
|
169
|
+
def reserve(self, label, expected_load=None, limit=0, ttl=120):
|
|
170
|
+
with self.db() as db:
|
|
171
|
+
db.execute("BEGIN IMMEDIATE")
|
|
172
|
+
self.prune(db)
|
|
173
|
+
row = db.execute(
|
|
174
|
+
"SELECT options FROM account_options WHERE label=?", (label,)
|
|
175
|
+
).fetchone()
|
|
176
|
+
options = json.loads(row[0]) if row else {}
|
|
177
|
+
if maintenance_active(options) or not options.get("enabled", True):
|
|
178
|
+
return False
|
|
179
|
+
configured = options.get("max_sessions", 0)
|
|
180
|
+
limit = (
|
|
181
|
+
min(limit, configured) if limit and configured else limit or configured
|
|
182
|
+
)
|
|
183
|
+
current = db.execute(
|
|
184
|
+
"SELECT count(*) FROM sessions WHERE label=? AND id<>?",
|
|
185
|
+
(label, self.session),
|
|
186
|
+
).fetchone()[0]
|
|
187
|
+
current += db.execute(
|
|
188
|
+
"SELECT count(*) FROM reservations WHERE label=? AND owner<>?",
|
|
189
|
+
(label, self.session),
|
|
190
|
+
).fetchone()[0]
|
|
191
|
+
own = db.execute(
|
|
192
|
+
"SELECT label FROM sessions WHERE id=?", (self.session,)
|
|
193
|
+
).fetchone()
|
|
194
|
+
compare = current + (1 if own and own[0] == label else 0)
|
|
195
|
+
if (
|
|
196
|
+
limit
|
|
197
|
+
and current >= limit
|
|
198
|
+
or expected_load is not None
|
|
199
|
+
and compare > expected_load
|
|
200
|
+
):
|
|
201
|
+
return False
|
|
202
|
+
# Reserve before any network call. No inference is moved until login succeeds.
|
|
203
|
+
db.execute(
|
|
204
|
+
"INSERT OR IGNORE INTO sessions VALUES(?,?,?)",
|
|
205
|
+
(self.session, os.getpid(), ""),
|
|
206
|
+
)
|
|
207
|
+
db.execute(
|
|
208
|
+
"INSERT OR REPLACE INTO reservations VALUES(?,?,?)",
|
|
209
|
+
(self.session, label, self.clock() + ttl),
|
|
210
|
+
)
|
|
211
|
+
self.touch()
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
def begin_maintenance(self, label):
|
|
215
|
+
with self.db() as db:
|
|
216
|
+
db.execute("BEGIN IMMEDIATE")
|
|
217
|
+
self.prune(db)
|
|
218
|
+
if (
|
|
219
|
+
db.execute("SELECT 1 FROM sessions WHERE label=?", (label,)).fetchone()
|
|
220
|
+
or db.execute(
|
|
221
|
+
"SELECT 1 FROM reservations WHERE label=?", (label,)
|
|
222
|
+
).fetchone()
|
|
223
|
+
):
|
|
224
|
+
return False
|
|
225
|
+
row = db.execute(
|
|
226
|
+
"SELECT options FROM account_options WHERE label=?", (label,)
|
|
227
|
+
).fetchone()
|
|
228
|
+
options = json.loads(row[0]) if row else {}
|
|
229
|
+
if maintenance_active(options):
|
|
230
|
+
return False
|
|
231
|
+
options.update(
|
|
232
|
+
maintenance=True,
|
|
233
|
+
maintenance_pid=os.getpid(),
|
|
234
|
+
maintenance_birth=process_birth(os.getpid()),
|
|
235
|
+
)
|
|
236
|
+
db.execute(
|
|
237
|
+
"INSERT OR REPLACE INTO account_options VALUES(?,?)",
|
|
238
|
+
(label, json.dumps(options)),
|
|
239
|
+
)
|
|
240
|
+
self.signal("accounts")
|
|
241
|
+
self.signal()
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
def release_reservation(self):
|
|
245
|
+
with self.db() as db:
|
|
246
|
+
db.execute("DELETE FROM reservations WHERE owner=?", (self.session,))
|
|
247
|
+
|
|
248
|
+
def renew_reservation(self, ttl=120):
|
|
249
|
+
with self.db() as db:
|
|
250
|
+
db.execute(
|
|
251
|
+
"UPDATE reservations SET until=? WHERE owner=? AND until>?",
|
|
252
|
+
(self.clock() + ttl, self.session, self.clock()),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def touch(self, **details):
|
|
256
|
+
with self.db() as db:
|
|
257
|
+
row = db.execute(
|
|
258
|
+
"SELECT details FROM session_details WHERE id=?", (self.session,)
|
|
259
|
+
).fetchone()
|
|
260
|
+
value = json.loads(row[0]) if row else {}
|
|
261
|
+
value.update(details)
|
|
262
|
+
db.execute(
|
|
263
|
+
"INSERT OR REPLACE INTO session_details VALUES(?,?,?,?)",
|
|
264
|
+
(
|
|
265
|
+
self.session,
|
|
266
|
+
process_birth(os.getpid()),
|
|
267
|
+
self.clock(),
|
|
268
|
+
json.dumps(value),
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def detail(self):
|
|
273
|
+
with self.db() as db:
|
|
274
|
+
self.prune(db)
|
|
275
|
+
rows = db.execute(
|
|
276
|
+
"SELECT s.id,s.pid,s.label,d.seen,d.details FROM sessions s LEFT JOIN session_details d ON s.id=d.id"
|
|
277
|
+
).fetchall()
|
|
278
|
+
return [
|
|
279
|
+
{
|
|
280
|
+
**json.loads(r[4] or "{}"),
|
|
281
|
+
"id": r[0],
|
|
282
|
+
"pid": r[1],
|
|
283
|
+
"account": r[2],
|
|
284
|
+
"seen": r[3],
|
|
285
|
+
}
|
|
286
|
+
for r in rows
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
def options(self, label):
|
|
290
|
+
with self.db() as db:
|
|
291
|
+
row = db.execute(
|
|
292
|
+
"SELECT options FROM account_options WHERE label=?", (label,)
|
|
293
|
+
).fetchone()
|
|
294
|
+
value = {
|
|
295
|
+
"enabled": True,
|
|
296
|
+
"priority": 0,
|
|
297
|
+
"max_sessions": 0,
|
|
298
|
+
"models": [],
|
|
299
|
+
**(json.loads(row[0]) if row else {}),
|
|
300
|
+
}
|
|
301
|
+
if value.get("maintenance") and not maintenance_active(value):
|
|
302
|
+
value["maintenance"] = False
|
|
303
|
+
return value
|
|
304
|
+
|
|
305
|
+
def set_options(self, label, **values):
|
|
306
|
+
value = self.options(label)
|
|
307
|
+
value.update(values)
|
|
308
|
+
with self.db() as db:
|
|
309
|
+
db.execute(
|
|
310
|
+
"INSERT OR REPLACE INTO account_options VALUES(?,?)",
|
|
311
|
+
(label, json.dumps(value)),
|
|
312
|
+
)
|
|
313
|
+
self.signal("accounts")
|
|
314
|
+
self.signal()
|
|
315
|
+
|
|
316
|
+
def signal(self, name="wake"):
|
|
317
|
+
with self.db() as db:
|
|
318
|
+
db.execute(
|
|
319
|
+
"INSERT INTO signals VALUES(?,1) ON CONFLICT(name) DO UPDATE SET revision=revision+1",
|
|
320
|
+
(name,),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def revision(self, name="wake"):
|
|
324
|
+
with self.db() as db:
|
|
325
|
+
row = db.execute(
|
|
326
|
+
"SELECT revision FROM signals WHERE name=?", (name,)
|
|
327
|
+
).fetchone()
|
|
328
|
+
return row[0] if row else 0
|
|
329
|
+
|
|
330
|
+
def intent(self, thread, details=None):
|
|
331
|
+
with self.db() as db:
|
|
332
|
+
if details is None:
|
|
333
|
+
row = db.execute(
|
|
334
|
+
"SELECT details FROM intents WHERE owner=? AND thread=?",
|
|
335
|
+
(self.session, thread),
|
|
336
|
+
).fetchone()
|
|
337
|
+
return json.loads(row[0]) if row else None
|
|
338
|
+
db.execute(
|
|
339
|
+
"INSERT OR REPLACE INTO intents VALUES(?,?,?)",
|
|
340
|
+
(self.session, thread, json.dumps(details)),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def record(self, kind, **details):
|
|
344
|
+
# Callers use fixed reason codes and metadata, never raw exceptions/payloads.
|
|
345
|
+
with self.db() as db:
|
|
346
|
+
import settings
|
|
347
|
+
|
|
348
|
+
limit = settings.load(self.state)["event_limit"]
|
|
349
|
+
db.execute(
|
|
350
|
+
"INSERT INTO events(at,owner,kind,details) VALUES(?,?,?,?)",
|
|
351
|
+
(self.clock(), self.session, kind, json.dumps(details)),
|
|
352
|
+
)
|
|
353
|
+
db.execute(
|
|
354
|
+
"DELETE FROM events WHERE id NOT IN (SELECT id FROM events ORDER BY id DESC LIMIT ?)",
|
|
355
|
+
(limit,),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def command(self, owner, op, details=None):
|
|
359
|
+
with self.db() as db:
|
|
360
|
+
cursor = db.execute(
|
|
361
|
+
"INSERT INTO commands(owner,op,details,at,status) VALUES(?,?,?,?,?)",
|
|
362
|
+
(owner, op, json.dumps(details or {}), self.clock(), "pending"),
|
|
363
|
+
)
|
|
364
|
+
self.signal()
|
|
365
|
+
return cursor.lastrowid
|
|
366
|
+
|
|
367
|
+
def take_commands(self):
|
|
368
|
+
with self.db() as db:
|
|
369
|
+
db.execute("BEGIN IMMEDIATE")
|
|
370
|
+
rows = db.execute(
|
|
371
|
+
"SELECT id,op,details FROM commands WHERE owner=? AND status='pending' ORDER BY id",
|
|
372
|
+
(self.session,),
|
|
373
|
+
).fetchall()
|
|
374
|
+
for row in rows:
|
|
375
|
+
db.execute(
|
|
376
|
+
"UPDATE commands SET status='received' WHERE id=?", (row[0],)
|
|
377
|
+
)
|
|
378
|
+
return [(r[0], r[1], json.loads(r[2])) for r in rows]
|
|
379
|
+
|
|
380
|
+
def ack(self, command, status):
|
|
381
|
+
with self.db() as db:
|
|
382
|
+
db.execute("UPDATE commands SET status=? WHERE id=?", (status, command))
|
|
383
|
+
|
|
384
|
+
def claim(self, thread):
|
|
385
|
+
with self.db() as db:
|
|
386
|
+
db.execute("BEGIN IMMEDIATE")
|
|
387
|
+
self.prune(db)
|
|
388
|
+
row = db.execute(
|
|
389
|
+
"SELECT owner FROM threads WHERE thread=?", (thread,)
|
|
390
|
+
).fetchone()
|
|
391
|
+
if row and row[0] != self.session:
|
|
392
|
+
return False
|
|
393
|
+
db.execute(
|
|
394
|
+
"INSERT OR REPLACE INTO threads VALUES(?,?,?)",
|
|
395
|
+
(thread, self.session, os.getpid()),
|
|
396
|
+
)
|
|
397
|
+
return True
|
|
398
|
+
|
|
399
|
+
def owner(self, thread):
|
|
400
|
+
with self.db() as db:
|
|
401
|
+
self.prune(db)
|
|
402
|
+
row = db.execute(
|
|
403
|
+
"SELECT owner FROM threads WHERE thread=?", (thread,)
|
|
404
|
+
).fetchone()
|
|
405
|
+
return row[0] if row else None
|
|
406
|
+
|
|
407
|
+
def release_thread(self, thread):
|
|
408
|
+
with self.db() as db:
|
|
409
|
+
db.execute(
|
|
410
|
+
"DELETE FROM threads WHERE thread=? AND owner=?", (thread, self.session)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def close(self):
|
|
414
|
+
with self.db() as db:
|
|
415
|
+
db.execute("DELETE FROM sessions WHERE id=?", (self.session,))
|
|
416
|
+
db.execute("DELETE FROM threads WHERE owner=?", (self.session,))
|
|
417
|
+
db.execute("DELETE FROM reservations WHERE owner=?", (self.session,))
|
|
418
|
+
|
|
419
|
+
def save(self, label, snapshot):
|
|
420
|
+
with self.db() as db:
|
|
421
|
+
db.execute(
|
|
422
|
+
"INSERT OR REPLACE INTO usage VALUES(?,?,?)",
|
|
423
|
+
(label, self.clock(), json.dumps(snapshot)),
|
|
424
|
+
)
|
|
425
|
+
db.execute(
|
|
426
|
+
"INSERT INTO history(at,label,snapshot) VALUES(?,?,?)",
|
|
427
|
+
(self.clock(), label, json.dumps(snapshot)),
|
|
428
|
+
)
|
|
429
|
+
import settings
|
|
430
|
+
|
|
431
|
+
db.execute(
|
|
432
|
+
"DELETE FROM history WHERE at<?",
|
|
433
|
+
(self.clock() - settings.load(self.state)["history_days"] * 86400,),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def cached(self, label, max_age=60, not_before=None):
|
|
437
|
+
with self.db() as db:
|
|
438
|
+
row = db.execute(
|
|
439
|
+
"SELECT checked,snapshot FROM usage WHERE label=?", (label,)
|
|
440
|
+
).fetchone()
|
|
441
|
+
if (
|
|
442
|
+
row
|
|
443
|
+
and self.clock() - row[0] < max_age
|
|
444
|
+
and (not_before is None or row[0] >= not_before)
|
|
445
|
+
):
|
|
446
|
+
return json.loads(row[1])
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
def mark(self, label, model=None, *, snapshot=None, reason="quota", delay=300):
|
|
450
|
+
if snapshot is not None:
|
|
451
|
+
self.save(label, snapshot)
|
|
452
|
+
until = (
|
|
453
|
+
reset_time(snapshot or {}, model, self.clock())
|
|
454
|
+
if reason == "quota"
|
|
455
|
+
else None
|
|
456
|
+
)
|
|
457
|
+
until = until or self.clock() + delay
|
|
458
|
+
scope = model or "*"
|
|
459
|
+
with self.db() as db:
|
|
460
|
+
db.execute(
|
|
461
|
+
"INSERT OR REPLACE INTO blocks VALUES(?,?,?,?)",
|
|
462
|
+
(label, scope, until, reason),
|
|
463
|
+
)
|
|
464
|
+
return until
|
|
465
|
+
|
|
466
|
+
def deadline(self, label, model=None):
|
|
467
|
+
with self.db() as db:
|
|
468
|
+
rows = db.execute(
|
|
469
|
+
"SELECT until FROM blocks WHERE label=? AND (scope='*' OR scope=?)",
|
|
470
|
+
(label, model or "*"),
|
|
471
|
+
).fetchall()
|
|
472
|
+
return max((row[0] for row in rows), default=0)
|
|
473
|
+
|
|
474
|
+
def invalidate(self, label):
|
|
475
|
+
with self.db() as db:
|
|
476
|
+
db.execute("DELETE FROM usage WHERE label=?", (label,))
|
|
477
|
+
|
|
478
|
+
def clear(self, label, model=None):
|
|
479
|
+
with self.db() as db:
|
|
480
|
+
db.execute(
|
|
481
|
+
"DELETE FROM blocks WHERE label=? AND (scope='*' OR scope=?)",
|
|
482
|
+
(label, model or "*"),
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def reset_identity(self, label):
|
|
486
|
+
with self.db() as db:
|
|
487
|
+
for table in ("usage", "blocks", "history"):
|
|
488
|
+
db.execute(f"DELETE FROM {table} WHERE label=?", (label,))
|
|
489
|
+
self.signal()
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def process_birth(pid):
|
|
493
|
+
if pid == os.getpid():
|
|
494
|
+
return _own_birth()
|
|
495
|
+
return _birth(pid)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def maintenance_active(options):
|
|
499
|
+
if not options.get("maintenance"):
|
|
500
|
+
return False
|
|
501
|
+
pid = options.get("maintenance_pid")
|
|
502
|
+
return (
|
|
503
|
+
pid is None
|
|
504
|
+
or Pool.alive(pid)
|
|
505
|
+
and process_birth(pid) == options.get("maintenance_birth")
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _birth(pid):
|
|
510
|
+
try:
|
|
511
|
+
return subprocess.run(
|
|
512
|
+
["ps", "-p", str(pid), "-o", "lstart="],
|
|
513
|
+
capture_output=True,
|
|
514
|
+
text=True,
|
|
515
|
+
timeout=2,
|
|
516
|
+
).stdout.strip()
|
|
517
|
+
except (OSError, subprocess.SubprocessError):
|
|
518
|
+
return ""
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
@functools.lru_cache(maxsize=1)
|
|
522
|
+
def _own_birth():
|
|
523
|
+
return _birth(os.getpid())
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
CAPACITY_MESSAGE = "Selected model is at capacity. Please try a different model."
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def failure_kind(info, message=None):
|
|
530
|
+
if info == "policyCapacityUnavailable":
|
|
531
|
+
return "capacity"
|
|
532
|
+
# Older/generic error envelopes can omit the enum. Match only the known
|
|
533
|
+
# terminal capacity message, never broad words like "policy" or "capacity".
|
|
534
|
+
if (
|
|
535
|
+
info in (None, "other", "serverOverloaded", "flexUnavailable")
|
|
536
|
+
and isinstance(message, str)
|
|
537
|
+
and message.strip().removeprefix("■").strip() == CAPACITY_MESSAGE
|
|
538
|
+
):
|
|
539
|
+
return "capacity"
|
|
540
|
+
if isinstance(info, str):
|
|
541
|
+
if info == "usageLimitExceeded":
|
|
542
|
+
return "quota"
|
|
543
|
+
if info == "unauthorized":
|
|
544
|
+
return "auth"
|
|
545
|
+
if info in {
|
|
546
|
+
"rateLimitExceeded",
|
|
547
|
+
"internalServerError",
|
|
548
|
+
"serverOverloaded",
|
|
549
|
+
"flexUnavailable",
|
|
550
|
+
}:
|
|
551
|
+
return "transient"
|
|
552
|
+
return None
|
|
553
|
+
if isinstance(info, dict):
|
|
554
|
+
for key in [
|
|
555
|
+
"httpConnectionFailed",
|
|
556
|
+
"responseStreamConnectionFailed",
|
|
557
|
+
"responseStreamDisconnected",
|
|
558
|
+
"responseTooManyFailedAttempts",
|
|
559
|
+
]:
|
|
560
|
+
if key in info:
|
|
561
|
+
status = (info[key] or {}).get("httpStatusCode")
|
|
562
|
+
if status == 401:
|
|
563
|
+
return "auth"
|
|
564
|
+
if (
|
|
565
|
+
status is None
|
|
566
|
+
or isinstance(status, int)
|
|
567
|
+
and (
|
|
568
|
+
status in (408, 429)
|
|
569
|
+
or 500 <= status < 600
|
|
570
|
+
or status == 200
|
|
571
|
+
and key
|
|
572
|
+
in {
|
|
573
|
+
"responseStreamDisconnected",
|
|
574
|
+
"responseTooManyFailedAttempts",
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
):
|
|
578
|
+
return "transient"
|
|
579
|
+
return None
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Verify installed native login controls using fake identities, with network denied.
|
|
2
|
+
|
|
3
|
+
This does not verify server authorization, model generation, or quota failover.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import base64
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
import argparse
|
|
13
|
+
|
|
14
|
+
from router import RPC, codex_path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def synthetic_token(account):
|
|
18
|
+
def encode(value):
|
|
19
|
+
return base64.urlsafe_b64encode(json.dumps(value).encode()).decode().rstrip("=")
|
|
20
|
+
|
|
21
|
+
return ".".join(
|
|
22
|
+
[
|
|
23
|
+
encode({"alg": "none", "typ": "JWT"}),
|
|
24
|
+
encode(
|
|
25
|
+
{
|
|
26
|
+
"exp": int(time.time()) + 3600,
|
|
27
|
+
"email": f"{account}@example.invalid",
|
|
28
|
+
"https://api.openai.com/auth": {
|
|
29
|
+
"chatgpt_account_id": account,
|
|
30
|
+
"chatgpt_user_id": account,
|
|
31
|
+
"chatgpt_plan_type": "plus",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
),
|
|
35
|
+
"fake-signature",
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
parser = argparse.ArgumentParser()
|
|
42
|
+
parser.add_argument("--binary", default=None)
|
|
43
|
+
args = parser.parse_args()
|
|
44
|
+
with tempfile.TemporaryDirectory(prefix="codex-accounts-offline-probe-") as folder:
|
|
45
|
+
env = dict(os.environ)
|
|
46
|
+
env["CODEX_HOME"] = folder
|
|
47
|
+
for key in ("OPENAI_API_KEY", "CODEX_API_KEY", "CODEX_ACCESS_TOKEN"):
|
|
48
|
+
env.pop(key, None)
|
|
49
|
+
proc = await asyncio.create_subprocess_exec(
|
|
50
|
+
"/usr/bin/sandbox-exec",
|
|
51
|
+
"-p",
|
|
52
|
+
"(version 1) (allow default) (deny network*)",
|
|
53
|
+
args.binary or codex_path(),
|
|
54
|
+
"app-server",
|
|
55
|
+
"--stdio",
|
|
56
|
+
"-c",
|
|
57
|
+
'cli_auth_credentials_store="ephemeral"',
|
|
58
|
+
env=env,
|
|
59
|
+
stdin=asyncio.subprocess.PIPE,
|
|
60
|
+
stdout=asyncio.subprocess.PIPE,
|
|
61
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
62
|
+
)
|
|
63
|
+
rpc = RPC(proc)
|
|
64
|
+
pid = proc.pid
|
|
65
|
+
try:
|
|
66
|
+
await rpc.initialize()
|
|
67
|
+
for account in ("synthetic-one", "synthetic-two"):
|
|
68
|
+
result = await rpc.call(
|
|
69
|
+
"account/login/start",
|
|
70
|
+
{
|
|
71
|
+
"type": "chatgptAuthTokens",
|
|
72
|
+
"accessToken": synthetic_token(account),
|
|
73
|
+
"chatgptAccountId": account,
|
|
74
|
+
"chatgptPlanType": "plus",
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
assert result.get("type") == "chatgptAuthTokens"
|
|
78
|
+
assert proc.pid == pid and proc.returncode is None
|
|
79
|
+
print(
|
|
80
|
+
"PASS: native app-server accepted two synthetic login changes in one process."
|
|
81
|
+
)
|
|
82
|
+
print("Network denied by OS sandbox. No real accounts or model calls used.")
|
|
83
|
+
print(
|
|
84
|
+
"Not verified: real authentication, thread continuation, or actual quota handoff."
|
|
85
|
+
)
|
|
86
|
+
finally:
|
|
87
|
+
await rpc.close()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Reap detached children while their launcher remains alive."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def spawn(*args, **kwargs):
|
|
8
|
+
process = subprocess.Popen(*args, **kwargs)
|
|
9
|
+
threading.Thread(
|
|
10
|
+
target=process.wait, daemon=True, name="codex-child-reaper"
|
|
11
|
+
).start()
|
|
12
|
+
return process
|