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.
@@ -0,0 +1,110 @@
1
+ """Validated local preferences; no automatic model fallback or extra billing."""
2
+
3
+ from pathlib import Path
4
+ import copy
5
+ import json
6
+ import os
7
+ import tomllib
8
+
9
+ DEFAULTS = {
10
+ "retry_interval": 300,
11
+ "retry_attempts": 3,
12
+ "unknown_reset_interval": 300,
13
+ "watchdog_interval": 30,
14
+ "idle_warning_after": 120,
15
+ "routing": "balanced",
16
+ "notifications": False,
17
+ "fallback_models": [],
18
+ "preferred_accounts": [],
19
+ "max_sessions_per_account": 0,
20
+ "reservation_seconds": 120,
21
+ "history_days": 30,
22
+ "event_limit": 2000,
23
+ "offline_retry": 30,
24
+ "background_on_disconnect": False,
25
+ "broker_idle_seconds": 600,
26
+ "keep_awake": False,
27
+ }
28
+
29
+
30
+ def validate(values):
31
+ unknown = set(values) - set(DEFAULTS)
32
+ if unknown:
33
+ raise ValueError("Unknown settings: " + ", ".join(sorted(unknown)))
34
+ for key, value in values.items():
35
+ expected = DEFAULTS[key]
36
+ if isinstance(expected, bool):
37
+ if not isinstance(value, bool):
38
+ raise ValueError(f"{key} must be true or false")
39
+ elif isinstance(expected, int):
40
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
41
+ raise ValueError(f"{key} must be a nonnegative integer")
42
+ if key not in {"max_sessions_per_account", "retry_attempts"} and value < 1:
43
+ raise ValueError(f"{key} must be positive")
44
+ elif isinstance(expected, list):
45
+ if not isinstance(value, list) or not all(
46
+ isinstance(v, str) for v in value
47
+ ):
48
+ raise ValueError(f"{key} must be a list of strings")
49
+ if values.get("routing", "balanced") not in {
50
+ "balanced",
51
+ "quota",
52
+ "priority",
53
+ "round-robin",
54
+ }:
55
+ raise ValueError("routing must be balanced, quota, priority, or round-robin")
56
+ if values.get("reservation_seconds", 120) < 30:
57
+ raise ValueError(
58
+ "reservation_seconds must be at least 30 to allow heartbeat renewal"
59
+ )
60
+ return values
61
+
62
+
63
+ def load(state, project=None, profile=None):
64
+ result = copy.deepcopy(DEFAULTS)
65
+ for path in [
66
+ state / "settings.json",
67
+ Path(project) / ".codex-accounts.toml" if project else None,
68
+ Path(project) / ".relay.toml" if project else None,
69
+ ]:
70
+ if not path or not path.exists():
71
+ continue
72
+ data = (
73
+ json.loads(path.read_text())
74
+ if path.suffix == ".json"
75
+ else tomllib.loads(path.read_text())
76
+ )
77
+ result.update(validate(data.get("settings", {})))
78
+ if profile:
79
+ result.update(validate(data.get("profiles", {}).get(profile, {})))
80
+ return result
81
+
82
+
83
+ def edit(state, key, raw, profile=None):
84
+ try:
85
+ value = json.loads(raw)
86
+ except ValueError:
87
+ value = raw
88
+ validate({key: value})
89
+ path = state / "settings.json"
90
+ data = json.loads(path.read_text()) if path.exists() else {}
91
+ target = (
92
+ data.setdefault("profiles", {}).setdefault(profile, {})
93
+ if profile
94
+ else data.setdefault("settings", {})
95
+ )
96
+ target[key] = value
97
+ atomic_json(path, data)
98
+
99
+
100
+ def atomic_json(path, data):
101
+ path = Path(path)
102
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
103
+ temporary = path.with_name(path.name + f".{os.getpid()}.tmp")
104
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
105
+ with os.fdopen(fd, "w") as stream:
106
+ json.dump(data, stream, indent=2)
107
+ stream.write("\n")
108
+ stream.flush()
109
+ os.fsync(stream.fileno())
110
+ temporary.replace(path)
@@ -0,0 +1,233 @@
1
+ """Local launch explanations, redacted diagnostics, saved workspaces, and cleanup."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import shutil
7
+ import sys
8
+ import time
9
+
10
+ from settings import atomic_json
11
+ from pool import Pool
12
+
13
+
14
+ def explain(arguments, cwd, binary=None):
15
+ from launch import (
16
+ translate,
17
+ working_directory,
18
+ native_mode,
19
+ additional_roots,
20
+ tokens,
21
+ arguments_only,
22
+ )
23
+ from router import VERSION
24
+
25
+ if binary is None:
26
+ from router import codex_path
27
+
28
+ try:
29
+ binary = codex_path()
30
+ except Exception:
31
+ binary = None
32
+ native = native_mode(arguments, binary)
33
+ _, overrides = translate(arguments)
34
+ model = next(
35
+ (
36
+ value
37
+ for name, value, _ in tokens(arguments_only(arguments))
38
+ if name == "--model"
39
+ ),
40
+ None,
41
+ )
42
+ return {
43
+ "relay_version": VERSION,
44
+ "mode": "native" if native else "managed",
45
+ "account_recovery": not native,
46
+ "cwd": working_directory(arguments, cwd),
47
+ "model": model or "Native Codex default",
48
+ "permissions": overrides or "Saved thread or native Codex defaults",
49
+ "additional_directories": additional_roots(
50
+ arguments, working_directory(arguments, cwd)
51
+ ),
52
+ "codex": binary,
53
+ "note": "Native utilities and external/local-provider endpoints retain native behavior."
54
+ if native
55
+ else "Relay manages account resets, temporary failures, and the saved conversation.",
56
+ }
57
+
58
+
59
+ def diagnostics(state, output):
60
+ """Allowlisted operational facts only; never read credentials or raw job arguments."""
61
+ from router import VERSION
62
+ import platform
63
+
64
+ pool = Pool(state)
65
+ try:
66
+ details = pool.detail()
67
+ with pool.db() as db:
68
+ health = db.execute("PRAGMA quick_check").fetchone()[0]
69
+ events = [
70
+ r[0]
71
+ for r in db.execute(
72
+ "SELECT kind FROM events ORDER BY id DESC LIMIT 100"
73
+ )
74
+ ]
75
+ value = {
76
+ "relay_version": VERSION,
77
+ "python": platform.python_version(),
78
+ "platform": sys.platform,
79
+ "database": "ok" if health == "ok" else "failed",
80
+ "active_sessions": len(details),
81
+ "session_states": [r.get("state", "unknown") for r in details],
82
+ "recent_event_types": events,
83
+ }
84
+ destination = Path(output).expanduser()
85
+ with destination.open("x", encoding="utf-8") as stream:
86
+ os.chmod(destination, 0o600)
87
+ json.dump(value, stream, indent=2)
88
+ stream.write("\n")
89
+ return str(destination)
90
+ finally:
91
+ pool.close()
92
+
93
+
94
+ def workspace(state, action, name=None, sessions=None, binary=None):
95
+ import service
96
+
97
+ path = state / "workspaces.json"
98
+ data = json.loads(path.read_text()) if path.exists() else {}
99
+ pool = Pool(state)
100
+ try:
101
+ if action == "list":
102
+ return {key: len(value) for key, value in data.items()}
103
+ if action == "remove":
104
+ data.pop(name, None)
105
+ atomic_json(path, data)
106
+ return {"removed": name}
107
+ if action == "save":
108
+ live = pool.detail()
109
+ chosen = [
110
+ r
111
+ for r in live
112
+ if not sessions or any(r["id"].startswith(s) for s in sessions)
113
+ ]
114
+ if not chosen or sessions and len(chosen) != len(sessions):
115
+ raise ValueError(
116
+ "Choose live session IDs from relay status, or omit IDs to save every live session"
117
+ )
118
+ jobs = []
119
+ for row in chosen:
120
+ job_path = service.folder(state) / (row["id"] + ".job.json")
121
+ if not job_path.exists():
122
+ continue
123
+ jobs.append(row["id"])
124
+ if not jobs:
125
+ raise ValueError("No supervised sessions were available to save")
126
+ data[name] = jobs
127
+ atomic_json(path, data)
128
+ return {"saved": name, "sessions": len(jobs)}
129
+ if name not in data:
130
+ raise ValueError("Unknown workspace")
131
+ alive = {r["id"] for r in pool.detail()}
132
+ result = []
133
+ for ident in data[name]:
134
+ if ident not in alive:
135
+ service.restart(state, ident)
136
+ result.append({"session": ident, "attach": "relay attach " + ident})
137
+ return result
138
+ finally:
139
+ pool.close()
140
+
141
+
142
+ def cleanup(state, *, apply=False, days=30, jobs=False):
143
+ """Default to a dry run. Preserve runtimes used by live or restartable jobs."""
144
+ import runtime
145
+
146
+ pool = Pool(state)
147
+ try:
148
+ live = {r["id"] for r in pool.detail()}
149
+ finally:
150
+ pool.close()
151
+ threshold = time.time() - days * 86400
152
+ protected = {str(Path(__file__).parent.resolve())}
153
+ workspaces_file = state / "workspaces.json"
154
+ saved_workspaces = (
155
+ json.loads(workspaces_file.read_text()) if workspaces_file.exists() else {}
156
+ )
157
+ workspace_jobs = {ident for group in saved_workspaces.values() for ident in group}
158
+ value = runtime.index(state)
159
+ native_protected = {
160
+ row["sha256"][:20] for row in value["runtimes"][-1:] if row.get("sha256")
161
+ }
162
+ if value.get("selected"):
163
+ native_protected.add(value["selected"][:20])
164
+ candidates = []
165
+ protect_all_python = False
166
+ for path in (state / "services").glob("*.job.json"):
167
+ try:
168
+ job = json.loads(path.read_text())
169
+ except (OSError, ValueError):
170
+ protect_all_python = True
171
+ continue
172
+ ident = path.name.removesuffix(".job.json")
173
+ old_stopped = (
174
+ ident not in live
175
+ and job.get("state") in {"stopped", "removed"}
176
+ and path.stat().st_mtime < threshold
177
+ )
178
+ if old_stopped and jobs and ident not in workspace_jobs:
179
+ candidates.extend(
180
+ p
181
+ for p in path.parent.glob(ident + ".*")
182
+ if p.is_file() and not p.is_symlink()
183
+ )
184
+ continue
185
+ if job.get("binary"):
186
+ protected.add(str(Path(job["binary"]).resolve()))
187
+ if job.get("relay_runtime"):
188
+ protected.add(str(Path(job["relay_runtime"]).resolve()))
189
+ elif ident in live:
190
+ protect_all_python = True
191
+ for folder in ["relay-runtimes", "native-runtimes"]:
192
+ root = state / folder
193
+ if not root.exists() or folder == "relay-runtimes" and protect_all_python:
194
+ continue
195
+ for path in root.iterdir():
196
+ resolved = str(path.resolve())
197
+ if (
198
+ path.is_symlink()
199
+ or not path.is_dir()
200
+ or path.stat().st_mtime >= threshold
201
+ ):
202
+ continue
203
+ if folder == "native-runtimes" and path.name in native_protected:
204
+ continue
205
+ if any(p == resolved or p.startswith(resolved + os.sep) for p in protected):
206
+ continue
207
+ candidates.append(path)
208
+ # Recheck live references immediately before mutations. Never delete from another user's state.
209
+ if apply:
210
+ guard = Pool(state)
211
+ try:
212
+ if {r["id"] for r in guard.detail()} != live:
213
+ raise ValueError("Sessions changed during cleanup; rerun the dry run")
214
+ finally:
215
+ guard.close()
216
+ for path in candidates:
217
+ if path.is_symlink():
218
+ raise ValueError(
219
+ "Cleanup encountered a changed symlink; nothing further was removed"
220
+ )
221
+ if path.is_dir():
222
+ shutil.rmtree(path)
223
+ else:
224
+ path.unlink(missing_ok=True)
225
+ value["runtimes"] = [
226
+ r for r in value["runtimes"] if Path(r["binary"]).is_file()
227
+ ]
228
+ atomic_json(state / "runtimes.json", value)
229
+ return {
230
+ "dry_run": not apply,
231
+ "paths": [str(p) for p in candidates],
232
+ "count": len(candidates),
233
+ }
@@ -0,0 +1,74 @@
1
+ """Wait for local filesystem changes; never fetch allowance or run inference."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import select
8
+
9
+
10
+ def fingerprint(value):
11
+ """Heartbeats and sample time do not represent a dashboard change."""
12
+ value = {**value, "at": None}
13
+ value["sessions"] = [
14
+ {k: v for k, v in item.items() if k != "seen"}
15
+ for item in value.get("sessions", [])
16
+ ]
17
+ return json.dumps(value, sort_keys=True)
18
+
19
+
20
+ class LocalChanges:
21
+ def __init__(self, state):
22
+ self.state = Path(state)
23
+ self.queue = select.kqueue() if hasattr(select, "kqueue") else None
24
+ self.files = []
25
+ self.signature = None
26
+
27
+ def close(self):
28
+ if self.queue:
29
+ self.queue.close()
30
+ for fd in self.files:
31
+ os.close(fd)
32
+ self.files.clear()
33
+
34
+ def wait(self, timeout=30):
35
+ paths = [
36
+ self.state,
37
+ self.state / "services",
38
+ self.state / "accounts",
39
+ self.state / "pool.sqlite",
40
+ self.state / "pool.sqlite-wal",
41
+ self.state / "settings.json",
42
+ ]
43
+ if not self.queue:
44
+ import time
45
+
46
+ time.sleep(timeout)
47
+ return
48
+ for fd in self.files:
49
+ os.close(fd)
50
+ self.files = []
51
+ registrations = []
52
+ for path in paths:
53
+ try:
54
+ fd = os.open(path, os.O_RDONLY)
55
+ except FileNotFoundError:
56
+ continue
57
+ self.files.append(fd)
58
+ registrations.append(
59
+ select.kevent(
60
+ fd,
61
+ filter=select.KQ_FILTER_VNODE,
62
+ flags=select.KQ_EV_ADD | select.KQ_EV_CLEAR,
63
+ fflags=select.KQ_NOTE_WRITE
64
+ | select.KQ_NOTE_RENAME
65
+ | select.KQ_NOTE_DELETE
66
+ | select.KQ_NOTE_ATTRIB,
67
+ )
68
+ )
69
+ self.queue.control(registrations, max(1, len(registrations)), timeout)
70
+
71
+ async def changed(self, timeout=30):
72
+ await asyncio.to_thread(self.wait, timeout)
73
+ # Coalesce a burst of updates into one consistent snapshot.
74
+ await asyncio.sleep(0.15)
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env python3
2
+ """Offline test double for native JSONL transport. Never contacts OpenAI."""
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import sys
8
+ import threading
9
+ import time
10
+ import uuid
11
+ import fcntl
12
+
13
+ home = Path(os.environ.get("CODEX_HOME", "."))
14
+ home.mkdir(parents=True, exist_ok=True)
15
+ state_file = home / "fake-history.json"
16
+ guard = threading.RLock()
17
+ out = threading.Lock()
18
+
19
+
20
+ def send(value):
21
+ with out:
22
+ print(json.dumps(value), flush=True)
23
+
24
+
25
+ def change(operation):
26
+ with guard:
27
+ with (home / "fake-history.lock").open("a") as lock:
28
+ fcntl.flock(lock, fcntl.LOCK_EX)
29
+ data = (
30
+ json.loads(state_file.read_text())
31
+ if state_file.exists()
32
+ else {"threads": {}, "calls": []}
33
+ )
34
+ result = operation(data)
35
+ temporary = state_file.with_suffix(".tmp")
36
+ temporary.write_text(json.dumps(data))
37
+ temporary.replace(state_file)
38
+ return result
39
+
40
+
41
+ def finish(thread, turn, text, mode):
42
+ status = "failed" if mode in {"quota", "capacity"} else "completed"
43
+ error = (
44
+ {"message": "synthetic limit", "codexErrorInfo": "usageLimitExceeded"}
45
+ if mode == "quota"
46
+ else None
47
+ )
48
+ if mode == "capacity":
49
+ error = {
50
+ "message": "Selected model is at capacity. Please try a different model.",
51
+ "codexErrorInfo": "policyCapacityUnavailable",
52
+ }
53
+
54
+ def apply(data):
55
+ value = data["threads"][thread]
56
+ value["status"] = {"type": "idle"}
57
+ value["turns"][-1].update(status=status, error=error)
58
+ return value["turns"][-1].copy()
59
+
60
+ value = change(apply)
61
+ if status == "completed":
62
+ send(
63
+ {
64
+ "method": "item/completed",
65
+ "params": {
66
+ "threadId": thread,
67
+ "item": {
68
+ "id": "reply-" + turn,
69
+ "type": "agentMessage",
70
+ "text": "FAKE_OK",
71
+ },
72
+ },
73
+ }
74
+ )
75
+ if mode == "capacity":
76
+ send(
77
+ {
78
+ "method": "error",
79
+ "params": {
80
+ "threadId": thread,
81
+ "turnId": turn,
82
+ "error": error,
83
+ "willRetry": False,
84
+ },
85
+ }
86
+ )
87
+ send({"method": "turn/completed", "params": {"threadId": thread, "turn": value}})
88
+ send(
89
+ {
90
+ "method": "thread/status/changed",
91
+ "params": {"threadId": thread, "status": {"type": "idle"}},
92
+ }
93
+ )
94
+
95
+
96
+ if "--version" in sys.argv:
97
+ print("codex-cli 0.166.0")
98
+ raise SystemExit()
99
+ if "--help" in sys.argv:
100
+ print("--remote --remote-auth-token-env")
101
+ raise SystemExit()
102
+
103
+ for raw in sys.stdin:
104
+ try:
105
+ request = json.loads(raw)
106
+ method = request.get("method")
107
+ params = request.get("params") or {}
108
+ ident = request.get("id")
109
+ if ident is None:
110
+ continue
111
+ change(
112
+ lambda d: d["calls"].append(
113
+ {"method": method, "thread": params.get("threadId")}
114
+ )
115
+ )
116
+ result = {}
117
+ if method == "initialize":
118
+ result = {
119
+ "userAgent": "fake/1",
120
+ "codexHome": str(home),
121
+ "platformFamily": "unix",
122
+ "platformOs": "macos",
123
+ }
124
+ elif method == "account/read":
125
+ result = {"account": {"type": "chatgpt", "planType": "plus"}}
126
+ elif method == "account/login/start":
127
+ result = {"type": params["type"]}
128
+ elif method == "account/rateLimits/read":
129
+ result = {
130
+ "ordinaryUsageAllowed": True,
131
+ "rateLimits": {
132
+ "primary": {"usedPercent": 20, "resetsAt": int(time.time()) + 3600}
133
+ },
134
+ }
135
+ elif method in ("thread/start", "thread/fork"):
136
+ thread = str(uuid.uuid4())
137
+ value = {
138
+ "id": thread,
139
+ "cwd": params.get("cwd", str(home)),
140
+ "model": params.get("model", "fake-model"),
141
+ "canAcceptDirectInput": True,
142
+ "status": {"type": "idle"},
143
+ "turns": [],
144
+ }
145
+ change(lambda d: d["threads"].update({thread: value}))
146
+ result = {
147
+ "thread": value,
148
+ "model": value["model"],
149
+ "cwd": value["cwd"],
150
+ "approvalPolicy": params.get("approvalPolicy", "never"),
151
+ "sandbox": params.get("sandbox", "read-only"),
152
+ }
153
+ send({"method": "thread/started", "params": {"thread": value}})
154
+ elif method == "thread/resume":
155
+ value = change(lambda d: d["threads"].get(params["threadId"]))
156
+ if not value:
157
+ raise ValueError("no thread")
158
+ if (
159
+ value.get("runtime") != os.getpid()
160
+ and value.get("status", {}).get("type") == "active"
161
+ ):
162
+ value["status"] = {"type": "idle"}
163
+ if value["turns"]:
164
+ value["turns"][-1].update(status="interrupted", error=None)
165
+ value.update({k: v for k, v in params.items() if k in {"model", "cwd"}})
166
+ change(lambda d: d["threads"].update({value["id"]: value}))
167
+ result = {"thread": value, "model": value["model"], "cwd": value["cwd"]}
168
+ elif method == "thread/read":
169
+ result = {"thread": change(lambda d: d["threads"][params["threadId"]])}
170
+ elif method == "thread/turns/list":
171
+ result = {
172
+ "data": change(
173
+ lambda d: list(reversed(d["threads"][params["threadId"]]["turns"]))[
174
+ : params.get("limit", 1)
175
+ ]
176
+ )
177
+ }
178
+ elif method == "thread/loaded/list":
179
+ result = {"data": change(lambda d: list(d["threads"]))}
180
+ elif method in {"turn/start", "review/start"}:
181
+ thread = params["threadId"]
182
+ turn = str(uuid.uuid4())
183
+ value = {"id": turn, "status": "inProgress", "items": [], "error": None}
184
+
185
+ def begin(data):
186
+ data["threads"][thread]["turns"].append(value)
187
+ data["threads"][thread]["status"] = {
188
+ "type": "active",
189
+ "activeFlags": [],
190
+ }
191
+ data["threads"][thread]["runtime"] = os.getpid()
192
+ return sum(
193
+ c["method"] in {"turn/start", "review/start"} for c in data["calls"]
194
+ )
195
+
196
+ count = change(begin)
197
+ fault = (
198
+ json.loads((home / "fault.json").read_text())
199
+ if (home / "fault.json").exists()
200
+ else {}
201
+ )
202
+ mode = "quota" if count <= fault.get("quota_turns", 0) else "ok"
203
+ if count <= fault.get("capacity_turns", 0):
204
+ mode = "capacity"
205
+ if not fault.get("drop_ack"):
206
+ send({"id": ident, "result": {"turn": value}})
207
+ send(
208
+ {
209
+ "method": "turn/started",
210
+ "params": {"threadId": thread, "turn": value},
211
+ }
212
+ )
213
+ if fault.get("external"):
214
+ send(
215
+ {
216
+ "method": "item/started",
217
+ "params": {
218
+ "threadId": thread,
219
+ "item": {"id": "external1", "type": "commandExecution"},
220
+ },
221
+ }
222
+ )
223
+ if fault.get("die_once") and count == 1:
224
+ os._exit(11)
225
+ timer = threading.Timer(
226
+ float(fault.get("delay", 0.02)), finish, args=(thread, turn, "", mode)
227
+ )
228
+ timer.daemon = True
229
+ timer.start()
230
+ continue
231
+ elif method == "turn/interrupt":
232
+ result = {}
233
+ send({"id": ident, "result": result})
234
+ except Exception:
235
+ send(
236
+ {
237
+ "id": request.get("id"),
238
+ "error": {"code": -32600, "message": "Synthetic request rejected"},
239
+ }
240
+ )