relay-codex 0.0.0-stage → 0.5.0

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/python/pool.py ADDED
@@ -0,0 +1,586 @@
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 checked_at(self, label):
437
+ with self.db() as db:
438
+ row = db.execute(
439
+ "SELECT checked FROM usage WHERE label=?", (label,)
440
+ ).fetchone()
441
+ return row[0] if row else None
442
+
443
+ def cached(self, label, max_age=60, not_before=None):
444
+ with self.db() as db:
445
+ row = db.execute(
446
+ "SELECT checked,snapshot FROM usage WHERE label=?", (label,)
447
+ ).fetchone()
448
+ if (
449
+ row
450
+ and self.clock() - row[0] < max_age
451
+ and (not_before is None or row[0] >= not_before)
452
+ ):
453
+ return json.loads(row[1])
454
+ return None
455
+
456
+ def mark(self, label, model=None, *, snapshot=None, reason="quota", delay=300):
457
+ if snapshot is not None:
458
+ self.save(label, snapshot)
459
+ until = (
460
+ reset_time(snapshot or {}, model, self.clock())
461
+ if reason == "quota"
462
+ else None
463
+ )
464
+ until = until or self.clock() + delay
465
+ scope = model or "*"
466
+ with self.db() as db:
467
+ db.execute(
468
+ "INSERT OR REPLACE INTO blocks VALUES(?,?,?,?)",
469
+ (label, scope, until, reason),
470
+ )
471
+ return until
472
+
473
+ def deadline(self, label, model=None):
474
+ with self.db() as db:
475
+ rows = db.execute(
476
+ "SELECT until FROM blocks WHERE label=? AND (scope='*' OR scope=?)",
477
+ (label, model or "*"),
478
+ ).fetchall()
479
+ return max((row[0] for row in rows), default=0)
480
+
481
+ def invalidate(self, label):
482
+ with self.db() as db:
483
+ db.execute("DELETE FROM usage WHERE label=?", (label,))
484
+
485
+ def clear(self, label, model=None):
486
+ with self.db() as db:
487
+ db.execute(
488
+ "DELETE FROM blocks WHERE label=? AND (scope='*' OR scope=?)",
489
+ (label, model or "*"),
490
+ )
491
+
492
+ def reset_identity(self, label):
493
+ with self.db() as db:
494
+ for table in ("usage", "blocks", "history"):
495
+ db.execute(f"DELETE FROM {table} WHERE label=?", (label,))
496
+ self.signal()
497
+
498
+
499
+ def process_birth(pid):
500
+ if pid == os.getpid():
501
+ return _own_birth()
502
+ return _birth(pid)
503
+
504
+
505
+ def maintenance_active(options):
506
+ if not options.get("maintenance"):
507
+ return False
508
+ pid = options.get("maintenance_pid")
509
+ return (
510
+ pid is None
511
+ or Pool.alive(pid)
512
+ and process_birth(pid) == options.get("maintenance_birth")
513
+ )
514
+
515
+
516
+ def _birth(pid):
517
+ try:
518
+ return subprocess.run(
519
+ ["ps", "-p", str(pid), "-o", "lstart="],
520
+ capture_output=True,
521
+ text=True,
522
+ timeout=2,
523
+ ).stdout.strip()
524
+ except (OSError, subprocess.SubprocessError):
525
+ return ""
526
+
527
+
528
+ @functools.lru_cache(maxsize=1)
529
+ def _own_birth():
530
+ return _birth(os.getpid())
531
+
532
+
533
+ CAPACITY_MESSAGE = "Selected model is at capacity. Please try a different model."
534
+
535
+
536
+ def failure_kind(info, message=None):
537
+ if info == "policyCapacityUnavailable":
538
+ return "capacity"
539
+ # Older/generic error envelopes can omit the enum. Match only the known
540
+ # terminal capacity message, never broad words like "policy" or "capacity".
541
+ if (
542
+ info in (None, "other", "serverOverloaded", "flexUnavailable")
543
+ and isinstance(message, str)
544
+ and message.strip().removeprefix("■").strip() == CAPACITY_MESSAGE
545
+ ):
546
+ return "capacity"
547
+ if isinstance(info, str):
548
+ if info == "usageLimitExceeded":
549
+ return "quota"
550
+ if info == "unauthorized":
551
+ return "auth"
552
+ if info in {
553
+ "rateLimitExceeded",
554
+ "internalServerError",
555
+ "serverOverloaded",
556
+ "flexUnavailable",
557
+ }:
558
+ return "transient"
559
+ return None
560
+ if isinstance(info, dict):
561
+ for key in [
562
+ "httpConnectionFailed",
563
+ "responseStreamConnectionFailed",
564
+ "responseStreamDisconnected",
565
+ "responseTooManyFailedAttempts",
566
+ ]:
567
+ if key in info:
568
+ status = (info[key] or {}).get("httpStatusCode")
569
+ if status == 401:
570
+ return "auth"
571
+ if (
572
+ status is None
573
+ or isinstance(status, int)
574
+ and (
575
+ status in (408, 429)
576
+ or 500 <= status < 600
577
+ or status == 200
578
+ and key
579
+ in {
580
+ "responseStreamDisconnected",
581
+ "responseTooManyFailedAttempts",
582
+ }
583
+ )
584
+ ):
585
+ return "transient"
586
+ 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
@@ -0,0 +1,6 @@
1
+ attrs==26.1.0
2
+ jsonschema==4.26.0
3
+ jsonschema-specifications==2025.9.1
4
+ referencing==0.37.0
5
+ rpds-py==2026.9.1
6
+ websockets==15.0.1
@@ -0,0 +1,2 @@
1
+ websockets>=15,<16
2
+ jsonschema>=4.25,<5