fix(installer): retry git ops + reject placeholder api_key + sync public configs

User hit two bugs in production:
1. uclaude_install.sh ERROR: Command failed at line 82 — gitea returned
   transient HTTP 502 to `git fetch`, `2>/dev/null` masked stderr but
   ERR trap fired with cryptic message.
2. After install, claude model picker showed only 5 models (built-in
   defaults) instead of 19. Root cause: load_config() fell back to the
   PUBLIC sanitized patcher.config.json (api_key='YOUR_API_KEY') after
   remote fetch failed → claude API auth broken → custom models invisible.

Fixes:

claude/uclaude_install.sh:
- New retry_git() helper: 3 attempts, 5s backoff, loud diagnostic
- Existing-clone branch: retry_git wraps `git fetch` AND `git reset`
- Fallback: if fetch fails 3x on existing clone, nuke and re-clone fresh
  (incremental fetch breaks more often than full clone on flaky gitea)
- Secondary fetch (before updater): tolerates failure with `|| true`
  (we already have a working clone)

claude/uclaude_updater.py:
- _config_is_usable() guard: rejects {"api_key": "YOUR_API_KEY"} etc.
- load_config() retries remote 3x with backoff before falling back
- Removed local-file fallback (was loading public sanitized = bait)
- Cache-only fallback now (from previous successful fetch)

Public configs synced from canonical (api_key sanitized, models list
fully refreshed):
- claude/patcher.config.json: 17 → 19 models (+gpt-5.5, +gemini-3.1-pro etc)
- codex/codex_config.json: 4 → 5 models (+gpt-5.5)
- gemini/gemini_config.json: refreshed
- target_version: 2.1.112 → 2.1.119

Tests: tests/test_installer_robustness.py — 6 new GREEN guards.
Total: 196 → 207 GREEN.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
delta-cloud-208e
2026-04-25 18:25:43 +00:00
parent 134606839a
commit 47e1978bef
5 changed files with 105 additions and 48 deletions

View File

@@ -626,51 +626,70 @@ CONFIG_URL = "https://git.sensey24.ru/aibot777/unlimitedcoding-config/raw/branch
CONFIG_TOKEN = "cadffcb0a6a3be728ac1ff619bb40c86588f6837"
PLACEHOLDER_API_KEYS = {"YOUR_API_KEY", "PLACEHOLDER", "REDACTED", "", None}
def _config_is_usable(data):
"""Reject configs with placeholder api_key — public sanitized files
have api_key='YOUR_API_KEY' and would silently install with broken
auth + stale models list."""
if not isinstance(data, dict):
return False
return data.get("api_key") not in PLACEHOLDER_API_KEYS
def load_config():
"""Load patcher.config.json from private config repo (with token auth).
Falls back to local file if network is unavailable.
Falls back to LOCAL CACHE if network is unavailable. Refuses to use
public sanitized patcher.config.json (api_key='YOUR_API_KEY') — that
would silently install broken auth, masking the real failure.
"""
# 1. Try fetching from private repo
try:
import urllib.request
req = urllib.request.Request(
CONFIG_URL,
headers={"Authorization": f"token {CONFIG_TOKEN}"},
)
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read().decode("utf-8"))
# Cache locally for offline use
cache_path = os.path.join(SCRIPT_DIR, ".patcher.config.cache.json")
# 1. Try fetching from private repo (with retry on transient gitea 502)
last_err = None
for attempt in range(1, 4):
try:
with open(cache_path, "w") as f:
json.dump(data, f, indent=2)
except Exception:
pass
return data
except Exception as e:
eprint(f" {Y}Remote config fetch failed: {e}{D}")
import urllib.request
req = urllib.request.Request(
CONFIG_URL,
headers={"Authorization": f"token {CONFIG_TOKEN}"},
)
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read().decode("utf-8"))
if _config_is_usable(data):
# Cache locally for offline use
cache_path = os.path.join(SCRIPT_DIR, ".patcher.config.cache.json")
try:
with open(cache_path, "w") as f:
json.dump(data, f, indent=2)
except Exception:
pass
return data
eprint(f" {Y}Remote config returned placeholder api_key (attempt {attempt}/3){D}")
last_err = "placeholder api_key"
except Exception as e:
last_err = e
eprint(f" {Y}Remote config fetch failed (attempt {attempt}/3): {e}{D}")
if attempt < 3:
time.sleep(2 * attempt)
# 2. Fallback: cached copy
# 2. Fallback: cached copy from previous successful fetch
cache_path = os.path.join(SCRIPT_DIR, ".patcher.config.cache.json")
if os.path.isfile(cache_path):
try:
with open(cache_path, "r") as f:
eprint(f" {Y}Using cached config{D}")
return json.load(f)
cached = json.load(f)
if _config_is_usable(cached):
eprint(f" {Y}Using cached config (remote fetch failed){D}")
return cached
except Exception:
pass
# 3. Fallback: local file (legacy, will be removed)
config_path = os.path.join(SCRIPT_DIR, "patcher.config.json")
if os.path.isfile(config_path):
try:
with open(config_path, "r") as f:
return json.load(f)
except Exception:
pass
eprint(f" {R}patcher.config.json not found (remote or local){D}")
# 3. NO MORE local-file fallback — public patcher.config.json is sanitized
# with placeholder api_key. Using it would silently install broken auth.
eprint(f" {R}Cannot load config: remote unreachable AND no usable cache.{D}")
eprint(f" {R} Last error: {last_err}{D}")
eprint(f" {R} Try again later or set UCLAUDE_API_KEY env var manually.{D}")
return None