wirenboard-agent-vm/copilot_token.py
Evgeny Boger ca7594cbcf Extract copilot OAuth device flow to copilot_token.py
Move the embedded Python from _claude_vm_get_copilot_token into a
standalone copilot_token.py script, consistent with github_app_token_demo.py.

The script handles cache load (with liveness validation), device flow,
and cache save. The shell function is reduced to a one-liner call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 20:23:22 +03:00

151 lines
4.5 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Obtain and cache a GitHub OAuth token for the Copilot API.
Uses the OpenCode OAuth App (Ov23li8tweQw6odWQebz) with read:user scope.
This app is the only one that grants access to the full Copilot model list
(Claude, Gemini, GPT-5, etc.).
Usage:
python3 copilot_token.py <cache_file>
Exits 0 and prints the token to stdout on success.
Exits 1 on failure (device code expired, auth error, etc.).
The cache file is read first; if it contains a valid token it is used
without running the device flow. On success the token is written back to
the cache file for future invocations.
"""
import json
import os
import sys
import time
import urllib.error
import urllib.request
CLIENT_ID = "Ov23li8tweQw6odWQebz"
SCOPE = "read:user"
# Bypass any HTTP proxy for GitHub auth endpoints
_opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
def _post(url, params):
data = json.dumps(params).encode()
req = urllib.request.Request(
url, data=data,
headers={"Accept": "application/json", "Content-Type": "application/json"},
)
with _opener.open(req, timeout=10) as r:
return json.load(r)
def _load_cached(cache_file):
"""Return cached token string if present and valid, else None."""
try:
with open(cache_file) as f:
token = json.load(f).get("access_token", "")
except (FileNotFoundError, json.JSONDecodeError):
return None
if not token:
return None
# Validate against GitHub
req = urllib.request.Request("https://api.github.com/user")
req.add_header("Authorization", f"token {token}")
req.add_header("Accept", "application/vnd.github+json")
try:
with _opener.open(req, timeout=10) as r:
r.read()
except urllib.error.HTTPError as e:
if e.code == 401:
print(" Cached Copilot token rejected (401), discarding", file=sys.stderr)
try:
os.unlink(cache_file)
except OSError:
pass
return None
except Exception:
pass # Network error — keep token, let it fail later
return token
def _save(cache_file, token):
os.makedirs(os.path.dirname(cache_file), exist_ok=True)
with open(cache_file, "w") as f:
json.dump({"access_token": token}, f)
os.chmod(cache_file, 0o600)
def _device_flow(cache_file):
"""Run OAuth device flow. Prints token to stdout and exits 0 on success."""
resp = _post(
"https://github.com/login/device/code",
{"client_id": CLIENT_ID, "scope": SCOPE},
)
interval = resp.get("interval", 5)
print(file=sys.stderr)
print(" " + "=" * 50, file=sys.stderr)
print(f" Open: https://github.com/login/device", file=sys.stderr)
print(f" Enter: {resp['user_code']}", file=sys.stderr)
print(" " + "=" * 50, file=sys.stderr)
print(file=sys.stderr)
print(f" Waiting for authorization (expires in {resp['expires_in']}s)...", file=sys.stderr)
deadline = time.time() + resp["expires_in"]
while time.time() < deadline:
time.sleep(interval)
try:
token_resp = _post(
"https://github.com/login/oauth/access_token",
{
"client_id": CLIENT_ID,
"device_code": resp["device_code"],
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
},
)
except Exception:
continue
if "access_token" in token_resp:
token = token_resp["access_token"]
_save(cache_file, token)
print(" Authorization successful!", file=sys.stderr)
print(token)
sys.exit(0)
error = token_resp.get("error", "")
if error == "slow_down":
interval = token_resp.get("interval", interval + 5)
elif error == "authorization_pending":
continue
else:
print(f" Error: {error}", file=sys.stderr)
sys.exit(1)
print(" Error: device code expired", file=sys.stderr)
sys.exit(1)
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <cache_file>", file=sys.stderr)
sys.exit(1)
cache_file = sys.argv[1]
token = _load_cached(cache_file)
if token:
print("Using cached Copilot token", file=sys.stderr)
print(token)
sys.exit(0)
print("Requesting GitHub token for Copilot API access...", file=sys.stderr)
_device_flow(cache_file)
if __name__ == "__main__":
main()