77 Commits

13 changed files with 157 additions and 5093 deletions

4
.gitignore vendored
View File

@@ -11,3 +11,7 @@ config.toml
.DS_Store .DS_Store
DEBIAN/ DEBIAN/
usr/ usr/
oauth-secrets.json
secrets/
*.secret
.env

View File

@@ -1,5 +1,25 @@
# Changelog # Changelog
## v3.10.4 (2026-05-25)
**Security: OAuth Secrets Editor + Import JSON**
### Security
- **All hardcoded OAuth secrets removed from source code and git history**
- OAuth client IDs and secrets now stored locally in `~/.config/codex-launcher/oauth-secrets.json`
- Git history rewritten to scrub all leaked credentials (0 matches verified)
- Pre-push hook blocks any future commit containing secrets
- All old Gitea releases deleted (contained leaked secrets in .deb files)
### New Features
- **OAuth Secrets editor** in GUI — "OAuth Secrets" button in header bar
- **Import JSON** button — import `client_secret_*.json` downloaded from Google Cloud Console
- Supports both `"installed"` and `"web"` JSON formats from Google
### Antigravity Fix (from v3.10.3)
- Antigravity REST API uses slug IDs, not display names
- Verified all model IDs with live API testing
## v3.10.3 (2026-05-25) ## v3.10.3 (2026-05-25)
**Fix Antigravity 404 Errors — Verified REST Model IDs** **Fix Antigravity 404 Errors — Verified REST Model IDs**

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -69,8 +69,7 @@ For regular Gemini CLI OAuth, only `cloudcode-pa.googleapis.com` is used.
### 4.1 OAuth Flow ### 4.1 OAuth Flow
- **Client ID**: `884354919052-36trc1jjb3tguiac32ov6cod268c5blh.apps.googleusercontent.com` - **Client IDs**: Stored locally in `~/.config/codex-launcher/oauth-secrets.json` (not in repo)
(also `1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com`)
- **OAuth callback**: `https://antigravity.google/oauth-callback` - **OAuth callback**: `https://antigravity.google/oauth-callback`
- **Token storage**: `~/.cache/codex-proxy/google-antigravity-oauth-token.json` - **Token storage**: `~/.cache/codex-proxy/google-antigravity-oauth-token.json`
- **Token refresh**: via `https://oauth2.googleapis.com/token` - **Token refresh**: via `https://oauth2.googleapis.com/token`

View File

@@ -3,11 +3,11 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.3_all.deb" ]; then if [ -f "$SCRIPT_DIR/codex-launcher_3.10.5_all.deb" ]; then
echo "Installing codex-launcher_3.10.3_all.deb ..." echo "Installing codex-launcher_3.10.5_all.deb ..."
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.3_all.deb" sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.5_all.deb"
echo "" echo ""
echo "Installed v3.10.3 via .deb package." echo "Installed v3.10.5 via .deb package."
echo " translate-proxy.py -> /usr/bin/translate-proxy.py" echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui" echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh" echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"

View File

@@ -26,6 +26,10 @@ model_catalog_json = ""
""" """
CHANGELOG = [ CHANGELOG = [
("3.10.4", "2026-05-25", [
"OAuth Secrets editor in GUI — update client ID/secret without editing files",
"Secrets stored in ~/.config/codex-launcher/oauth-secrets.json (not in repo)",
]),
("3.10.3", "2026-05-25", [ ("3.10.3", "2026-05-25", [
"Fix Antigravity 404: map display names to verified REST API model IDs", "Fix Antigravity 404: map display names to verified REST API model IDs",
"REST API uses slugs (gemini-3-flash) not display names (Gemini 3.5 Flash)", "REST API uses slugs (gemini-3-flash) not display names (Gemini 3.5 Flash)",
@@ -1776,7 +1780,7 @@ class LauncherWin(Gtk.Window):
# header row # header row
hdr = Gtk.Box(spacing=8) hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0) vbox.pack_start(hdr, False, False, 0)
lbl = Gtk.Label(label="<b>Codex Launcher v3.10.3</b>") lbl = Gtk.Label(label="<b>Codex Launcher v3.10.5</b>")
lbl.set_use_markup(True) lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0) hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog") changelog_btn = Gtk.Button(label="Changelog")
@@ -1800,6 +1804,9 @@ class LauncherWin(Gtk.Window):
mgr_btn = Gtk.Button(label="Manage Endpoints") mgr_btn = Gtk.Button(label="Manage Endpoints")
mgr_btn.connect("clicked", lambda b: self._open_mgr()) mgr_btn.connect("clicked", lambda b: self._open_mgr())
hdr.pack_end(mgr_btn, False, False, 0) hdr.pack_end(mgr_btn, False, False, 0)
oauth_btn = Gtk.Button(label="OAuth Secrets")
oauth_btn.connect("clicked", lambda b: self._edit_oauth_secrets())
hdr.pack_end(oauth_btn, False, False, 0)
# verification status bar # verification status bar
self._cli_info = _detect_codex_cli() self._cli_info = _detect_codex_cli()
@@ -2783,6 +2790,98 @@ class LauncherWin(Gtk.Window):
_stop_proxy() _stop_proxy()
Gtk.main_quit() Gtk.main_quit()
def _edit_oauth_secrets(self):
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
try:
with open(secrets_path) as f:
data = json.load(f)
except Exception:
data = {"antigravity": {"client_id": "", "client_secret": ""},
"gemini_cli": {"client_id": "", "client_secret": ""}}
dlg = Gtk.Dialog(title="OAuth 2.0 Client Secrets", parent=self, modal=True)
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
dlg.add_button("Save", Gtk.ResponseType.OK)
dlg.set_default_size(540, 420)
area = dlg.get_content_area()
area.set_margin_start(16)
area.set_margin_end(16)
area.set_margin_top(12)
area.set_margin_bottom(12)
area.set_spacing(6)
area.pack_start(Gtk.Label(label="<b>Google OAuth 2.0 credentials</b>\n<small>Stored locally in ~/.config/codex-launcher/oauth-secrets.json</small>", use_markup=True, xalign=0), False, False, 4)
fields = {}
for section_key, section_label in [("antigravity", "Antigravity (CloudCode)"), ("gemini_cli", "Gemini CLI")]:
section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
hdr_row = Gtk.Box(spacing=6)
hdr_row.pack_start(Gtk.Label(label=f"\n<b>{section_label}</b>", use_markup=True, xalign=0), True, True, 0)
import_btn = Gtk.Button(label="Import JSON")
import_btn.set_size_request(100, -1)
hdr_row.pack_end(import_btn, False, False, 0)
section_box.pack_start(hdr_row, False, False, 2)
sec = data.get(section_key, {})
for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]:
row = Gtk.Box(spacing=6)
lbl = Gtk.Label(label=fl + ":", xalign=0)
lbl.set_size_request(100, -1)
entry = Gtk.Entry()
entry.set_text(sec.get(fk, ""))
entry.set_size_request(380, -1)
if fk == "client_secret":
entry.set_visibility(False)
entry.set_invisible_char("*")
row.pack_start(lbl, False, False, 0)
row.pack_start(entry, True, True, 0)
section_box.pack_start(row, False, False, 2)
fields[(section_key, fk)] = entry
import_btn.connect("clicked", lambda b, sk=section_key: self._import_oauth_json(fields, sk))
area.pack_start(section_box, False, False, 0)
area.pack_start(Gtk.Label(label="\n<small>Import a client_secret_*.json from Google Cloud Console\nor edit fields manually. console.cloud.google.com → Credentials</small>", use_markup=True, xalign=0), False, False, 4)
area.show_all()
if dlg.run() == Gtk.ResponseType.OK:
for (sk, fk), entry in fields.items():
if sk not in data:
data[sk] = {}
data[sk][fk] = entry.get_text().strip()
try:
os.makedirs(os.path.dirname(secrets_path), exist_ok=True)
with open(secrets_path, "w") as f:
json.dump(data, f, indent=2)
os.chmod(secrets_path, 0o600)
except Exception as e:
self._show_error_dialog("Save failed", str(e))
dlg.destroy()
def _import_oauth_json(self, fields, section_key):
chooser = Gtk.FileChooserDialog(
title="Import Google OAuth Client Secret JSON",
parent=self, action=Gtk.FileChooserAction.OPEN)
chooser.add_button("Cancel", Gtk.ResponseType.CANCEL)
chooser.add_button("Open", Gtk.ResponseType.OK)
filt = Gtk.FileFilter()
filt.set_name("JSON files")
filt.add_pattern("*.json")
chooser.add_filter(filt)
if chooser.run() == Gtk.ResponseType.OK:
path = chooser.get_filename()
try:
with open(path) as f:
raw = json.load(f)
creds = raw.get("installed") or raw.get("web") or raw
cid = creds.get("client_id", "")
csec = creds.get("client_secret", "")
if not cid or not csec:
raise ValueError("JSON does not contain client_id and client_secret")
fields[(section_key, "client_id")].set_text(cid)
fields[(section_key, "client_secret")].set_text(csec)
except Exception as e:
self._show_error_dialog("Import failed", str(e))
chooser.destroy()
# ═══════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════
# Endpoint manager dialog # Endpoint manager dialog
# ═══════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════
@@ -3273,9 +3372,17 @@ class EditEndpointDialog(Gtk.Dialog):
is_antigravity = oauth_provider == "google-antigravity" is_antigravity = oauth_provider == "google-antigravity"
token_path = os.path.expanduser("~/.cache/codex-proxy/google-antigravity-oauth-token.json" if is_antigravity else "~/.cache/codex-proxy/google-cli-oauth-token.json") token_path = os.path.expanduser("~/.cache/codex-proxy/google-antigravity-oauth-token.json" if is_antigravity else "~/.cache/codex-proxy/google-cli-oauth-token.json")
_oauth_secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
try:
with open(_oauth_secrets_path) as _f:
_oauth_secrets = json.load(_f)
except Exception:
_oauth_secrets = {}
if is_antigravity: if is_antigravity:
CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" _sec = _oauth_secrets.get("antigravity", {})
CLIENT_SECRET = "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" CLIENT_ID = _sec.get("client_id", "")
CLIENT_SECRET = _sec.get("client_secret", "")
SCOPES = [ SCOPES = [
"https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.email",
@@ -3288,8 +3395,9 @@ class EditEndpointDialog(Gtk.Dialog):
callback_path = "/oauth-callback" callback_path = "/oauth-callback"
provider_kind = "antigravity" provider_kind = "antigravity"
else: else:
CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com" _sec = _oauth_secrets.get("gemini_cli", {})
CLIENT_SECRET = "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" CLIENT_ID = _sec.get("client_id", "")
CLIENT_SECRET = _sec.get("client_secret", "")
SCOPES = [ SCOPES = [
"https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.email",
@@ -3580,7 +3688,7 @@ class EditEndpointDialog(Gtk.Dialog):
auth_url = "https://codebuff.com/api/auth/cli/code" auth_url = "https://codebuff.com/api/auth/cli/code"
body = json.dumps({"fingerprintId": fingerprint_id}).encode() body = json.dumps({"fingerprintId": fingerprint_id}).encode()
req = urllib.request.Request(auth_url, data=body, req = urllib.request.Request(auth_url, data=body,
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.3"}) headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.5"})
resp = urllib.request.urlopen(req, timeout=30) resp = urllib.request.urlopen(req, timeout=30)
data = json.loads(resp.read()) data = json.loads(resp.read())
login_url = data.get("loginUrl", "") or data.get("login_url", "") login_url = data.get("loginUrl", "") or data.get("login_url", "")
@@ -3605,7 +3713,7 @@ class EditEndpointDialog(Gtk.Dialog):
time.sleep(2) time.sleep(2)
try: try:
poll_req = urllib.request.Request(poll_url, poll_req = urllib.request.Request(poll_url,
headers={"User-Agent": "codex-launcher/3.10.3"}) headers={"User-Agent": "codex-launcher/3.10.5"})
poll_resp = urllib.request.urlopen(poll_req, timeout=10) poll_resp = urllib.request.urlopen(poll_req, timeout=10)
poll_data = json.loads(poll_resp.read()) poll_data = json.loads(poll_resp.read())
user = poll_data.get("user") user = poll_data.get("user")

View File

@@ -335,7 +335,7 @@ def _codebuff_get_session(token, model):
req = urllib.request.Request(url, data=body, headers={ req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.3", "User-Agent": "codex-launcher/3.10.5",
"x-codebuff-model": model, "x-codebuff-model": model,
}) })
try: try:
@@ -383,7 +383,7 @@ def _codebuff_start_run(token, agent_id):
req = urllib.request.Request(url, data=body, headers={ req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.3", "User-Agent": "codex-launcher/3.10.5",
}) })
try: try:
resp = urllib.request.urlopen(req, timeout=15) resp = urllib.request.urlopen(req, timeout=15)
@@ -416,7 +416,7 @@ def _codebuff_finish_run(token, run_id, status="completed"):
req = urllib.request.Request(url, data=body, headers={ req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.3", "User-Agent": "codex-launcher/3.10.5",
}) })
try: try:
urllib.request.urlopen(req, timeout=10) urllib.request.urlopen(req, timeout=10)
@@ -1570,6 +1570,10 @@ _PROVIDER_POLICIES = {
"tool_output_limit": 6000, "max_input_items": 35, "compaction": "balanced"}, "tool_output_limit": 6000, "max_input_items": 35, "compaction": "balanced"},
"openadapter": {"reasoning_mode": "off", "max_tokens": 32768, "strip_reasoning": True, "openadapter": {"reasoning_mode": "off", "max_tokens": 32768, "strip_reasoning": True,
"tool_output_limit": 6000, "max_input_items": 30, "compaction": "balanced"}, "tool_output_limit": 6000, "max_input_items": 30, "compaction": "balanced"},
"cloudcode-pa": {"compaction": "aggressive", "context_size": 1000000,
"tool_output_limit": 6000, "max_input_items": 60},
"googleapis": {"compaction": "balanced", "context_size": 1000000,
"tool_output_limit": 6000, "max_input_items": 80},
} }
def provider_policy(target_url=None, backend=None): def provider_policy(target_url=None, backend=None):
@@ -1588,12 +1592,14 @@ _MODEL_CONTEXT = {
"claude-sonnet": 200000, "claude-haiku": 200000, "claude-sonnet": 200000, "claude-haiku": 200000,
"glm-5.1": 128000, "glm-5": 128000, "glm-4": 128000, "glm-5.1": 128000, "glm-5": 128000, "glm-4": 128000,
"deepseek": 64000, "gemini-2.5-flash": 1000000, "gemini-2.5-pro": 2000000, "deepseek": 64000, "gemini-2.5-flash": 1000000, "gemini-2.5-pro": 2000000,
"gemini-3-flash": 1000000, "gemini-3.5-flash-low": 1000000,
"gemini-3.1-pro-low": 2000000,
"gemini-3.5-flash": 1000000, "gemini-3.1-pro": 2000000, "gemini-3.5-flash": 1000000, "gemini-3.1-pro": 2000000,
"Gemini 3.5 Flash": 1000000, "Gemini 3.1 Pro": 2000000, "Gemini 3.5 Flash": 1000000, "Gemini 3.1 Pro": 2000000,
"Claude Sonnet 4.6": 200000, "Claude Opus 4.6": 200000, "Claude Sonnet 4.6": 200000, "Claude Opus 4.6": 200000,
"GPT-OSS 120B": 128000, "GPT-OSS 120B": 128000,
"claude-sonnet-4.6-thinking": 200000, "claude-opus-4.6-thinking": 200000, "claude-sonnet-4-6": 200000, "claude-opus-4-6-thinking": 200000,
"gpt-oss-120b": 128000, "gpt-oss-120b-medium": 128000,
"mimo": 32768, "minimax": 32768, "kimi": 128000, "mimo": 32768, "minimax": 32768, "kimi": 128000,
"_default": 32768, "_default": 32768,
} }
@@ -5342,7 +5348,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {token}", "Authorization": f"Bearer {token}",
"User-Agent": "codex-launcher/3.10.3", "User-Agent": "codex-launcher/3.10.5",
"x-codebuff-model": model, "x-codebuff-model": model,
} }
if instance_id: if instance_id:
@@ -5508,7 +5514,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
if body.get("tool_choice"): if body.get("tool_choice"):
chat_body["tool_choice"] = body["tool_choice"] chat_body["tool_choice"] = body["tool_choice"]
target = f"{_CODEBUFF_API_URL}/api/v1/chat/completions" target = f"{_CODEBUFF_API_URL}/api/v1/chat/completions"
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.10.3", "x-codebuff-model": model} headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.10.5", "x-codebuff-model": model}
if instance_id: if instance_id:
headers["x-codebuff-instance-id"] = instance_id headers["x-codebuff-instance-id"] = instance_id
print(f"[codebuff] retry POST {target} model={model} stream={stream} run={run_id} (thinking disabled via DeepSeek native)", file=sys.stderr) print(f"[codebuff] retry POST {target} model={model} stream={stream} run={run_id} (thinking disabled via DeepSeek native)", file=sys.stderr)