sarveshpatel commited on
Commit
19499ad
·
verified ·
1 Parent(s): f1d5dce

Upload 12 files

Browse files
Files changed (7) hide show
  1. Dockerfile +6 -2
  2. README.md +11 -1
  3. app.py +31 -1
  4. chatscript.py +76 -0
  5. codex_engine.py +37 -12
  6. singlescript.py +77 -0
  7. start.sh +22 -14
Dockerfile CHANGED
@@ -16,10 +16,14 @@ RUN npm install -g @openai/codex && codex --version
16
  # uid 1000, so reuse it instead of creating a duplicate.
17
  ENV HOME=/home/node \
18
  PATH=/home/node/.local/bin:$PATH \
19
- # Point Codex at the persistent bucket so auth.json + sessions survive restarts.
20
- CODEX_HOME=/data/.codex \
 
 
 
21
  # Defaults (override via Space variables/secrets).
22
  CODEX_SANDBOX=workspace-write \
 
23
  PORT=7860
24
 
25
  WORKDIR /app
 
16
  # uid 1000, so reuse it instead of creating a duplicate.
17
  ENV HOME=/home/node \
18
  PATH=/home/node/.local/bin:$PATH \
19
+ # CODEX_HOME on FAST LOCAL disk (Codex's SQLite I/O on a network bucket is the
20
+ # main latency killer). Use /tmp — always writable on HF Spaces regardless of
21
+ # the runtime user. auth.json is seeded from / synced back to the bucket.
22
+ CODEX_HOME=/tmp/.codex \
23
+ AUTH_PERSIST_DIR=/data/.codex \
24
  # Defaults (override via Space variables/secrets).
25
  CODEX_SANDBOX=workspace-write \
26
+ CODEX_EFFORT=low \
27
  PORT=7860
28
 
29
  WORKDIR /app
README.md CHANGED
@@ -63,7 +63,17 @@ Optional **variables**:
63
  |---|---|---|
64
  | `CODEX_SANDBOX` | `workspace-write` | `read-only` for chat-only, `workspace-write` to let Codex edit files |
65
  | `CODEX_MODEL` | (unset) | pin a Codex model, e.g. `gpt-5-codex` |
66
- | `CODEX_TIMEOUT` | `600` | max seconds per request |
 
 
 
 
 
 
 
 
 
 
67
 
68
  ### 3. Upload your login (`auth.json`)
69
  On your **local machine** (with a browser):
 
63
  |---|---|---|
64
  | `CODEX_SANDBOX` | `workspace-write` | `read-only` for chat-only, `workspace-write` to let Codex edit files |
65
  | `CODEX_MODEL` | (unset) | pin a Codex model, e.g. `gpt-5-codex` |
66
+ | `CODEX_TIMEOUT` | `180` | max seconds between Codex output events |
67
+ | `CODEX_MAX_CONCURRENCY` | `4` | max Codex turns running at once (resource cap) |
68
+ | `CODEX_QUEUE_TIMEOUT` | `90` | seconds a request waits in queue before `429` |
69
+
70
+ ### Concurrency
71
+
72
+ - Requests for **different** sessions run in parallel, up to `CODEX_MAX_CONCURRENCY`.
73
+ - Requests for the **same** session are **serialized** — two calls never resume the
74
+ same Codex thread or write the same workspace at once (prevents corruption).
75
+ - When all slots are busy and the queue wait exceeds `CODEX_QUEUE_TIMEOUT`, the API
76
+ returns **HTTP 429** so clients can back off and retry.
77
 
78
  ### 3. Upload your login (`auth.json`)
79
  On your **local machine** (with a browser):
app.py CHANGED
@@ -21,6 +21,7 @@ import asyncio
21
  import json
22
  import os
23
  import re
 
24
  import time
25
  import uuid
26
  from contextlib import aclosing
@@ -46,12 +47,17 @@ except ImportError:
46
  # Config
47
  # --------------------------------------------------------------------------- #
48
  CODEX_BIN = os.environ.get("CODEX_BIN", "codex") # on the Space this is `codex`
49
- CODEX_HOME = os.environ.get("CODEX_HOME", "/data/.codex")
 
 
 
 
50
  AUTH_FILE = Path(CODEX_HOME) / "auth.json"
51
  SESSIONS_ROOT = Path(os.environ.get("SESSIONS_ROOT", "/data/sessions"))
52
  API_TOKEN = os.environ.get("API_TOKEN", "") # HF secret; if empty, auth is OPEN
53
  DEFAULT_SANDBOX = os.environ.get("CODEX_SANDBOX", "workspace-write") # or read-only
54
  CODEX_MODEL = os.environ.get("CODEX_MODEL", "").strip() # optional override
 
55
  READ_TIMEOUT = float(os.environ.get("CODEX_TIMEOUT", "180")) # per-output-gap secs
56
  # Max Codex turns running at once across all sessions (each is a heavy process).
57
  MAX_CONCURRENCY = int(os.environ.get("CODEX_MAX_CONCURRENCY", "4"))
@@ -143,6 +149,26 @@ def _check_auth(authorization: Optional[str]) -> None:
143
  raise HTTPException(status_code=401, detail="Invalid or missing API token.")
144
 
145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  def _require_login() -> None:
147
  if not AUTH_FILE.exists():
148
  raise HTTPException(
@@ -256,6 +282,7 @@ async def health():
256
  "auth_required": bool(API_TOKEN),
257
  "sandbox": DEFAULT_SANDBOX,
258
  "engine": "app-server",
 
259
  "max_concurrency": MAX_CONCURRENCY,
260
  "active_sessions": len(_SESSION_LOCKS),
261
  }
@@ -308,6 +335,7 @@ async def chat_completions(
308
  sandbox=DEFAULT_SANDBOX,
309
  model=CODEX_MODEL or None,
310
  read_timeout=READ_TIMEOUT,
 
311
  )
312
 
313
  if req.stream:
@@ -335,6 +363,7 @@ async def chat_completions(
335
  raise HTTPException(status_code=502, detail=f"Codex engine: {e}")
336
  finally:
337
  guard.release()
 
338
 
339
  return JSONResponse(_completion_payload("".join(content_parts), model_name, usage))
340
 
@@ -375,6 +404,7 @@ async def _sse_stream(turn, model: str, session_dir, include_usage: bool, guard)
375
  yield chunk({"content": f"\n\n[codex error: {e}]"})
376
  finally:
377
  guard.release()
 
378
 
379
  yield chunk({}, finish="stop")
380
  if include_usage:
 
21
  import json
22
  import os
23
  import re
24
+ import shutil
25
  import time
26
  import uuid
27
  from contextlib import aclosing
 
47
  # Config
48
  # --------------------------------------------------------------------------- #
49
  CODEX_BIN = os.environ.get("CODEX_BIN", "codex") # on the Space this is `codex`
50
+ # CODEX_HOME lives on FAST LOCAL disk (Codex hammers SQLite here every turn — a
51
+ # network bucket makes that brutally slow). auth.json is seeded from / synced
52
+ # back to AUTH_PERSIST_DIR (the /data bucket) so the login still persists.
53
+ CODEX_HOME = os.environ.get("CODEX_HOME", "/tmp/.codex")
54
+ AUTH_PERSIST_DIR = Path(os.environ.get("AUTH_PERSIST_DIR", "/data/.codex"))
55
  AUTH_FILE = Path(CODEX_HOME) / "auth.json"
56
  SESSIONS_ROOT = Path(os.environ.get("SESSIONS_ROOT", "/data/sessions"))
57
  API_TOKEN = os.environ.get("API_TOKEN", "") # HF secret; if empty, auth is OPEN
58
  DEFAULT_SANDBOX = os.environ.get("CODEX_SANDBOX", "workspace-write") # or read-only
59
  CODEX_MODEL = os.environ.get("CODEX_MODEL", "").strip() # optional override
60
+ CODEX_EFFORT = os.environ.get("CODEX_EFFORT", "low").strip() # minimal|low|medium|high
61
  READ_TIMEOUT = float(os.environ.get("CODEX_TIMEOUT", "180")) # per-output-gap secs
62
  # Max Codex turns running at once across all sessions (each is a heavy process).
63
  MAX_CONCURRENCY = int(os.environ.get("CODEX_MAX_CONCURRENCY", "4"))
 
149
  raise HTTPException(status_code=401, detail="Invalid or missing API token.")
150
 
151
 
152
+ # Codex rotates auth.json in CODEX_HOME (local disk) when it refreshes the token.
153
+ # Persist that rotated copy back to the bucket so the login survives restarts.
154
+ _last_auth_mtime = AUTH_FILE.stat().st_mtime if AUTH_FILE.exists() else 0.0
155
+
156
+
157
+ def _sync_auth_back() -> None:
158
+ try:
159
+ if not AUTH_FILE.exists():
160
+ return
161
+ global _last_auth_mtime
162
+ m = AUTH_FILE.stat().st_mtime
163
+ if m <= _last_auth_mtime:
164
+ return # unchanged since last sync — skip the (slow) bucket write
165
+ AUTH_PERSIST_DIR.mkdir(parents=True, exist_ok=True)
166
+ shutil.copy2(AUTH_FILE, AUTH_PERSIST_DIR / "auth.json")
167
+ _last_auth_mtime = m
168
+ except Exception:
169
+ pass # best-effort; never fail a request over this
170
+
171
+
172
  def _require_login() -> None:
173
  if not AUTH_FILE.exists():
174
  raise HTTPException(
 
282
  "auth_required": bool(API_TOKEN),
283
  "sandbox": DEFAULT_SANDBOX,
284
  "engine": "app-server",
285
+ "effort": CODEX_EFFORT,
286
  "max_concurrency": MAX_CONCURRENCY,
287
  "active_sessions": len(_SESSION_LOCKS),
288
  }
 
335
  sandbox=DEFAULT_SANDBOX,
336
  model=CODEX_MODEL or None,
337
  read_timeout=READ_TIMEOUT,
338
+ effort=CODEX_EFFORT or None,
339
  )
340
 
341
  if req.stream:
 
363
  raise HTTPException(status_code=502, detail=f"Codex engine: {e}")
364
  finally:
365
  guard.release()
366
+ _sync_auth_back()
367
 
368
  return JSONResponse(_completion_payload("".join(content_parts), model_name, usage))
369
 
 
404
  yield chunk({"content": f"\n\n[codex error: {e}]"})
405
  finally:
406
  guard.release()
407
+ _sync_auth_back()
408
 
409
  yield chunk({}, finish="stop")
410
  if include_usage:
chatscript.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================================
2
+ # Codex-as-API -> live STREAMING chat (async, for Google Colab)
3
+ # HOW TO USE: open https://colab.research.google.com -> new notebook ->
4
+ # paste this WHOLE file into ONE cell -> Run. A "you>" box appears;
5
+ # type a message and watch the reply stream in token-by-token.
6
+ # Type exit (or quit) to stop.
7
+ # ============================================================================
8
+
9
+ # --- install (quiet) -------------------------------------------------------
10
+ !pip -q install openai nest_asyncio
11
+
12
+ import asyncio, nest_asyncio
13
+ from openai import AsyncOpenAI
14
+
15
+ nest_asyncio.apply() # lets asyncio run inside Colab's event loop
16
+
17
+ # --- YOUR API settings (edit these) ----------------------------------------
18
+ BASE_URL = "https://sarveshpatel-codex.hf.space/v1" # your Space + /v1
19
+ API_KEY = "CURSEOFWITCHER" # your API_TOKEN secret
20
+ MODEL = "codex"
21
+ SESSION = "colab-chat" # same string = the chat remembers context server-side
22
+
23
+ client = AsyncOpenAI(
24
+ base_url=BASE_URL,
25
+ api_key=API_KEY,
26
+ default_headers={"X-Session-Id": SESSION}, # persistent memory
27
+ )
28
+
29
+
30
+ async def stream_reply(user_text: str):
31
+ """Stream one assistant reply, printing tokens as they arrive."""
32
+ stream = await client.chat.completions.create(
33
+ model=MODEL,
34
+ messages=[{"role": "user", "content": user_text}],
35
+ stream=True,
36
+ )
37
+ print("ai > ", end="", flush=True)
38
+ full = []
39
+ async for chunk in stream:
40
+ if not chunk.choices:
41
+ continue
42
+ delta = chunk.choices[0].delta.content
43
+ if delta:
44
+ full.append(delta)
45
+ print(delta, end="", flush=True) # live streaming output
46
+ print("\n")
47
+ return "".join(full)
48
+
49
+
50
+ def chat():
51
+ """Interactive REPL. Each turn streams live; the session remembers context."""
52
+ print(f"Chatting with Codex (session='{SESSION}'). Type 'exit' to stop.\n")
53
+ while True:
54
+ try:
55
+ user = input("you> ").strip()
56
+ except (EOFError, KeyboardInterrupt):
57
+ print("\n[chat ended]")
58
+ break
59
+ if not user:
60
+ continue
61
+ if user.lower() in ("exit", "quit"):
62
+ print("[chat ended]")
63
+ break
64
+ asyncio.run(stream_reply(user))
65
+
66
+
67
+ # Start chatting.
68
+ chat()
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # NOTES:
72
+ # * Want a fresh conversation? change SESSION to a new string and re-run.
73
+ # * One-off (no memory)? set SESSION = None and remove default_headers.
74
+ # * Prefer a non-streaming single call? use stream=False and read
75
+ # resp.choices[0].message.content.
76
+ # ---------------------------------------------------------------------------
codex_engine.py CHANGED
@@ -37,6 +37,9 @@ async def _send(proc: asyncio.subprocess.Process, obj: dict) -> None:
37
  await proc.stdin.drain()
38
 
39
 
 
 
 
40
  async def run_turn(
41
  *,
42
  codex_bin: str,
@@ -47,6 +50,7 @@ async def run_turn(
47
  sandbox: str,
48
  model: Optional[str],
49
  read_timeout: float,
 
50
  ) -> AsyncIterator[dict]:
51
  """
52
  Async generator that drives one turn and yields events:
@@ -85,16 +89,20 @@ async def run_turn(
85
  except json.JSONDecodeError:
86
  continue # non-JSON log line on stdout — ignore
87
 
88
- async def await_response(req_id: int) -> dict:
89
- """Read until the response for req_id; ignore notifications meanwhile."""
90
  while True:
91
  msg = await read_msg()
92
  if msg is None:
93
  raise CodexError("app-server closed before responding")
94
  if msg.get("id") == req_id and ("result" in msg or "error" in msg):
95
- if "error" in msg:
96
- raise CodexError(f"app-server error: {msg['error']}")
97
- return msg.get("result", {})
 
 
 
 
98
 
99
  try:
100
  # 1) initialize handshake
@@ -116,7 +124,9 @@ async def run_turn(
116
  await await_response(0)
117
  await _send(proc, {"method": "initialized"})
118
 
119
- # 2) start or resume the thread
 
 
120
  if thread_id:
121
  await _send(proc, {
122
  "method": "thread/resume",
@@ -129,7 +139,11 @@ async def run_turn(
129
  "excludeTurns": True,
130
  },
131
  })
132
- else:
 
 
 
 
133
  params = {
134
  "cwd": str(workspace),
135
  "approvalPolicy": "never",
@@ -137,10 +151,10 @@ async def run_turn(
137
  }
138
  if model:
139
  params["model"] = model
140
- await _send(proc, {"method": "thread/start", "id": 1, "params": params})
 
 
141
 
142
- start_result = await await_response(1)
143
- tid = (start_result.get("thread") or {}).get("id")
144
  if not tid:
145
  raise CodexError("app-server did not return a thread id")
146
 
@@ -151,7 +165,9 @@ async def run_turn(
151
  }
152
  if model:
153
  turn_params["model"] = model
154
- await _send(proc, {"method": "turn/start", "id": 2, "params": turn_params})
 
 
155
 
156
  # 4) stream notifications until turn/completed
157
  delta_parts: list[str] = []
@@ -180,9 +196,18 @@ async def run_turn(
180
  "completion_tokens": last.get("outputTokens", 0) or 0,
181
  "total_tokens": last.get("totalTokens", 0) or 0,
182
  }
 
 
 
 
 
 
 
 
 
183
  elif method == "turn/completed":
184
  break
185
- elif msg.get("id") == 2 and "error" in msg:
186
  raise CodexError(f"turn error: {msg['error']}")
187
  elif msg.get("id") is not None and method is not None:
188
  # Server->client request (e.g. an approval). With approvalPolicy
 
37
  await proc.stdin.drain()
38
 
39
 
40
+ _AUTH_DEAD = ("session has ended", "log in again", "failed to refresh token")
41
+
42
+
43
  async def run_turn(
44
  *,
45
  codex_bin: str,
 
50
  sandbox: str,
51
  model: Optional[str],
52
  read_timeout: float,
53
+ effort: Optional[str] = None,
54
  ) -> AsyncIterator[dict]:
55
  """
56
  Async generator that drives one turn and yields events:
 
89
  except json.JSONDecodeError:
90
  continue # non-JSON log line on stdout — ignore
91
 
92
+ async def await_response_raw(req_id: int) -> dict:
93
+ """Read until the response for req_id; return the raw message (result or error)."""
94
  while True:
95
  msg = await read_msg()
96
  if msg is None:
97
  raise CodexError("app-server closed before responding")
98
  if msg.get("id") == req_id and ("result" in msg or "error" in msg):
99
+ return msg
100
+
101
+ async def await_response(req_id: int) -> dict:
102
+ msg = await await_response_raw(req_id)
103
+ if "error" in msg:
104
+ raise CodexError(f"app-server error: {msg['error']}")
105
+ return msg.get("result", {})
106
 
107
  try:
108
  # 1) initialize handshake
 
124
  await await_response(0)
125
  await _send(proc, {"method": "initialized"})
126
 
127
+ # 2) resume the thread if we have one; fall back to a fresh thread if the
128
+ # rollout is gone (e.g. CODEX_HOME is local and the Space restarted).
129
+ tid = None
130
  if thread_id:
131
  await _send(proc, {
132
  "method": "thread/resume",
 
139
  "excludeTurns": True,
140
  },
141
  })
142
+ resumed = await await_response_raw(1)
143
+ if "result" in resumed:
144
+ tid = (resumed["result"].get("thread") or {}).get("id")
145
+
146
+ if tid is None: # no thread_id, or resume failed -> start fresh
147
  params = {
148
  "cwd": str(workspace),
149
  "approvalPolicy": "never",
 
151
  }
152
  if model:
153
  params["model"] = model
154
+ await _send(proc, {"method": "thread/start", "id": 2, "params": params})
155
+ start_result = await await_response(2)
156
+ tid = (start_result.get("thread") or {}).get("id")
157
 
 
 
158
  if not tid:
159
  raise CodexError("app-server did not return a thread id")
160
 
 
165
  }
166
  if model:
167
  turn_params["model"] = model
168
+ if effort:
169
+ turn_params["effort"] = effort
170
+ await _send(proc, {"method": "turn/start", "id": 3, "params": turn_params})
171
 
172
  # 4) stream notifications until turn/completed
173
  delta_parts: list[str] = []
 
196
  "completion_tokens": last.get("outputTokens", 0) or 0,
197
  "total_tokens": last.get("totalTokens", 0) or 0,
198
  }
199
+ elif method == "error":
200
+ err = (msg.get("params") or {}).get("error", {}) or {}
201
+ blob = f"{err.get('message','')} {err.get('additionalDetails') or ''}".lower()
202
+ # Dead login: don't sit through 5x reconnect retries (~60s). Bail now.
203
+ if any(s in blob for s in _AUTH_DEAD):
204
+ raise CodexError(
205
+ "Codex login expired (session ended). Re-run `codex login` "
206
+ "and re-upload a fresh auth.json to /data/.codex/auth.json."
207
+ )
208
  elif method == "turn/completed":
209
  break
210
+ elif msg.get("id") == 3 and "error" in msg:
211
  raise CodexError(f"turn error: {msg['error']}")
212
  elif msg.get("id") is not None and method is not None:
213
  # Server->client request (e.g. an approval). With approvalPolicy
singlescript.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================================
2
+ # Codex-as-API -> OpenAI Agents SDK (personal "self AI" agent)
3
+ # HOW TO USE: open https://colab.research.google.com -> new notebook ->
4
+ # paste this WHOLE file into ONE cell -> press Run. That's it.
5
+ #
6
+ # It does 4 things:
7
+ # 1. installs the OpenAI Agents SDK
8
+ # 2. points the SDK at YOUR Codex API (running on your Hugging Face Space)
9
+ # 3. runs an agent and proves session memory works
10
+ # 4. leaves you a reusable ask("...") helper for the next cells
11
+ # ============================================================================
12
+
13
+ # --- 1) install (quiet) ----------------------------------------------------
14
+ !pip -q install openai-agents nest_asyncio
15
+
16
+ import asyncio, nest_asyncio
17
+ from openai import AsyncOpenAI
18
+ from agents import Agent, Runner, OpenAIChatCompletionsModel, set_tracing_disabled
19
+
20
+ nest_asyncio.apply() # lets asyncio.run() work inside Colab's loop
21
+
22
+ # --- 2) YOUR API settings (edit these) -------------------------------------
23
+ BASE_URL = "https://sarveshpatel-codex.hf.space/v1" # your Space + /v1
24
+ API_KEY = "CURSEOFWITCHER" # your API_TOKEN secret
25
+ MODEL = "codex" # always "codex"
26
+ SESSION = "colab-self-ai" # any string. Same string = persistent memory.
27
+
28
+ # --- 3) wire the Agents SDK to your Codex API ------------------------------
29
+ set_tracing_disabled(True) # don't send traces to OpenAI
30
+
31
+ client = AsyncOpenAI(
32
+ base_url=BASE_URL,
33
+ api_key=API_KEY,
34
+ default_headers={"X-Session-Id": SESSION}, # gives the agent memory
35
+ )
36
+ model = OpenAIChatCompletionsModel(model=MODEL, openai_client=client)
37
+
38
+ # This is your agent. Change `instructions` to define its personality/role.
39
+ self_ai = Agent(
40
+ name="SelfAI",
41
+ instructions=(
42
+ "You are SelfAI, my personal coding and research assistant powered by "
43
+ "Codex. Be concise, direct, and practical."
44
+ ),
45
+ model=model,
46
+ )
47
+
48
+ # --- 4) a reusable helper you can call in any later cell -------------------
49
+ def ask(prompt: str) -> str:
50
+ """Send a message to your agent and return its reply (remembers the session)."""
51
+ result = asyncio.run(Runner.run(self_ai, prompt))
52
+ return result.final_output
53
+
54
+
55
+ # --- 5) quick demo: connectivity + memory ----------------------------------
56
+ print("ping :", asyncio.run(
57
+ client.chat.completions.create(
58
+ model=MODEL, messages=[{"role": "user", "content": "Reply with exactly: PONG"}]
59
+ )
60
+ ).choices[0].message.content)
61
+
62
+ print("agent :", ask("In one sentence, what can you help me with?"))
63
+ print("memory>", ask("My favorite number is 42. Acknowledge in 3 words."))
64
+ print("recall:", ask("What is my favorite number? Reply with just the number."))
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # NEXT CELLS — just call ask():
68
+ # ask("Write a Python function to check if a string is a palindrome.")
69
+ # ask("Now add a test for it.") # it remembers the previous answer
70
+ #
71
+ # TIPS:
72
+ # * New conversation? change SESSION (or set it to None for one-off calls).
73
+ # * Streaming / tools: this API is OpenAI-compatible, so any OpenAI client
74
+ # works. Note: OpenAI-style function tools aren't forwarded yet — but
75
+ # Codex's own tools (running code, editing files) execute server-side
76
+ # inside the session.
77
+ # ---------------------------------------------------------------------------
start.sh CHANGED
@@ -1,25 +1,33 @@
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
 
4
- # CODEX_HOME points at the persistent bucket (/data/.codex). Make sure it exists.
5
- mkdir -p "${CODEX_HOME}" /data/sessions
 
 
 
 
6
 
7
- # Install the global safety rules into CODEX_HOME (refresh on every boot).
 
 
8
  cp -f /app/AGENTS.global.md "${CODEX_HOME}/AGENTS.md"
9
 
10
- # Codex needs a config.toml so it knows to use the ChatGPT login (not an API key).
11
- if [ ! -f "${CODEX_HOME}/config.toml" ]; then
12
- cat > "${CODEX_HOME}/config.toml" <<'EOF'
13
- preferred_auth_method = "chatgpt"
14
- EOF
 
 
 
15
  fi
16
 
17
- if [ -f "${CODEX_HOME}/auth.json" ]; then
18
- echo "[start] Found auth.json in CODEX_HOME — Codex login is ready."
19
- else
20
- echo "[start] WARNING: ${CODEX_HOME}/auth.json is MISSING."
21
- echo "[start] Run 'codex login' locally, then upload ~/.codex/auth.json to the"
22
- echo "[start] bucket at /data/.codex/auth.json. The API will return 503 until then."
23
  fi
24
 
25
  exec uvicorn app:app --host 0.0.0.0 --port "${PORT:-7860}"
 
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
 
4
+ # CODEX_HOME is on fast LOCAL disk; AUTH_PERSIST_DIR is the /data bucket where the
5
+ # login persists across restarts. Seed the local home from the bucket on boot.
6
+ # Default to /tmp (always writable on HF Spaces) if the env var is missing.
7
+ : "${CODEX_HOME:=/tmp/.codex}"
8
+ export CODEX_HOME
9
+ AUTH_PERSIST_DIR="${AUTH_PERSIST_DIR:-/data/.codex}"
10
 
11
+ mkdir -p "${CODEX_HOME}" "${AUTH_PERSIST_DIR}" /data/sessions
12
+
13
+ # Global safety rules (refresh every boot).
14
  cp -f /app/AGENTS.global.md "${CODEX_HOME}/AGENTS.md"
15
 
16
+ # Seed auth.json from the persistent bucket into the local CODEX_HOME.
17
+ if [ -f "${AUTH_PERSIST_DIR}/auth.json" ]; then
18
+ cp -f "${AUTH_PERSIST_DIR}/auth.json" "${CODEX_HOME}/auth.json"
19
+ echo "[start] Seeded auth.json from ${AUTH_PERSIST_DIR} — Codex login ready."
20
+ else
21
+ echo "[start] WARNING: ${AUTH_PERSIST_DIR}/auth.json is MISSING."
22
+ echo "[start] Run 'codex login' locally and upload ~/.codex/auth.json there."
23
+ echo "[start] The API returns 503 until it exists."
24
  fi
25
 
26
+ # Use the persisted config.toml if present; else write a minimal default.
27
+ if [ -f "${AUTH_PERSIST_DIR}/config.toml" ]; then
28
+ cp -f "${AUTH_PERSIST_DIR}/config.toml" "${CODEX_HOME}/config.toml"
29
+ elif [ ! -f "${CODEX_HOME}/config.toml" ]; then
30
+ printf 'preferred_auth_method = "chatgpt"\n' > "${CODEX_HOME}/config.toml"
 
31
  fi
32
 
33
  exec uvicorn app:app --host 0.0.0.0 --port "${PORT:-7860}"