Files
weval-l99/wevia-renew-pat.py
2026-04-19 15:48:31 +02:00

246 lines
8.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""
wevia-renew-pat.py — GitHub PAT auto-renewer via Selenium + Blade persistent Chrome profile.
Referenced in v82_tips_tokens (GitHub PAT auto-renew).
Usage:
python3 wevia-renew-pat.py [--token-name NAME] [--expiration-days 30]
Pattern (doctrine):
- Uses Chrome persistent profile C:/Users/Yace/AppData/Local/Google/Chrome/User Data
- Profile is ALREADY logged into GitHub (via Yacineutt session)
- Generates new PAT via GitHub UI (github.com/settings/tokens)
- Writes result to /var/www/html/api/blade-tasks/key_github_token_YYYYMMDD.json
Flow:
1. Launch headless Chrome with persistent profile
2. Navigate to github.com/settings/tokens/new
3. Fill form (token name, scopes, expiration)
4. Submit + extract token from post-creation page
5. Save to blade-tasks JSON + update secrets.env atomically
6. Delete/revoke previous token via API
Author: WEVIA Master / Opus WIRE (2026-04-18, Session B12)
"""
import sys
import os
import json
import argparse
import datetime
import subprocess
import tempfile
from pathlib import Path
# Default configuration
DEFAULT_TOKEN_NAME = "weval-auto-renewed"
DEFAULT_EXPIRATION_DAYS = 30
DEFAULT_SCOPES = ["repo", "workflow", "read:org"]
BLADE_TASKS_DIR = "/var/www/html/api/blade-tasks"
SECRETS_ENV = "/etc/weval/secrets.env"
LOG_DIR = "/opt/weval-l99/logs/pat-renew"
# Chrome profile path on Blade (documented in v82_tips_cyber)
# Reminder: this runs via Blade SSH executor, not directly from S204
BLADE_CHROME_PROFILE = r"C:/Users/Yace/AppData/Local/Google/Chrome/User Data"
def log(msg, level="INFO"):
"""Simple logger to stdout + log file."""
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{ts}] [{level}] {msg}"
print(line)
os.makedirs(LOG_DIR, exist_ok=True)
with open(f"{LOG_DIR}/pat-renew-{datetime.date.today().isoformat()}.log", "a") as f:
f.write(line + "\n")
def check_prereqs():
"""Verify required directories and environment."""
issues = []
if not os.path.isdir(BLADE_TASKS_DIR):
issues.append(f"Missing {BLADE_TASKS_DIR}")
if not os.path.isfile(SECRETS_ENV):
issues.append(f"Missing {SECRETS_ENV}")
return issues
def create_blade_task(token_name, expiration_days, scopes):
"""
Create a blade-task JSON that instructs Blade Chrome to renew the PAT.
Blade picks up the task, runs Selenium, writes back result.
"""
today = datetime.date.today().isoformat().replace("-", "")
task_id = f"pat_renew_{today}_{os.getpid()}"
task = {
"task_id": task_id,
"type": "github_pat_renew",
"status": "queued",
"created_at": datetime.datetime.utcnow().isoformat() + "Z",
"params": {
"token_name": token_name,
"expiration_days": expiration_days,
"scopes": scopes,
"target_url": "https://github.com/settings/tokens/new",
"chrome_profile": BLADE_CHROME_PROFILE,
},
"expected_output": {
"location": f"{BLADE_TASKS_DIR}/key_github_token_{today}.json",
"schema": {
"ok": "bool",
"token": "string (ghp_... or github_pat_...)",
"created_at": "ISO8601",
"expires_at": "ISO8601",
},
},
"doctrine_refs": [
"v82_tips_tokens/GitHub PAT auto-renew",
"v82_tips_cyber/Chrome persistent profile Yacineutt",
"doctrine supreme 0/WEVIA Master executor, Opus wires",
],
"safety": {
"zero_manual_powershell": True,
"zero_fake_data": True,
"persistent_profile_only": True,
"revoke_previous_after_success": True,
},
}
task_path = f"{BLADE_TASKS_DIR}/{task_id}.json"
with open(task_path, "w") as f:
json.dump(task, f, indent=2)
# Ensure www-data ownership (matches other blade-tasks)
try:
import pwd
uid = pwd.getpwnam("www-data").pw_uid
gid = pwd.getpwnam("www-data").pw_gid
os.chown(task_path, uid, gid)
except (KeyError, PermissionError):
pass
os.chmod(task_path, 0o644)
return task_id, task_path
def wait_for_result(task_id, timeout_seconds=300, poll_interval=10):
"""
Poll for the result file written by Blade.
Returns dict with result or None on timeout.
"""
today = datetime.date.today().isoformat().replace("-", "")
result_path = f"{BLADE_TASKS_DIR}/key_github_token_{today}.json"
import time
elapsed = 0
while elapsed < timeout_seconds:
if os.path.exists(result_path):
with open(result_path) as f:
data = json.load(f)
log(f"Result found after {elapsed}s at {result_path}")
return data
time.sleep(poll_interval)
elapsed += poll_interval
log(f"Waiting... {elapsed}/{timeout_seconds}s", "DEBUG")
return None
def update_secrets_env(new_token):
"""
Update /etc/weval/secrets.env with new GITHUB_PAT value.
Atomic write with .bak preservation. Requires root (via docker wrapper when needed).
"""
if not os.path.isfile(SECRETS_ENV):
log(f"secrets.env not writable ({SECRETS_ENV}) — skip update", "WARN")
return False
try:
with open(SECRETS_ENV) as f:
lines = f.readlines()
out = []
updated = False
for line in lines:
if line.strip().startswith("GITHUB_PAT="):
out.append(f"GITHUB_PAT={new_token}\n")
updated = True
else:
out.append(line)
if not updated:
out.append(f"GITHUB_PAT={new_token}\n")
# Atomic write
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir="/tmp") as tmp:
tmp.writelines(out)
tmp_path = tmp.name
# Backup then swap
bak = f"{SECRETS_ENV}.bak-pat-{datetime.date.today().isoformat()}"
subprocess.run(["cp", SECRETS_ENV, bak], check=False)
subprocess.run(["mv", tmp_path, SECRETS_ENV], check=True)
log(f"secrets.env updated, backup at {bak}")
return True
except PermissionError:
log("Permission denied on secrets.env — needs root/sudo wrapper", "ERROR")
return False
except Exception as e:
log(f"secrets.env update failed: {e}", "ERROR")
return False
def main():
parser = argparse.ArgumentParser(description="GitHub PAT auto-renewer via Blade Selenium")
parser.add_argument("--token-name", default=DEFAULT_TOKEN_NAME)
parser.add_argument("--expiration-days", type=int, default=DEFAULT_EXPIRATION_DAYS)
parser.add_argument("--scopes", nargs="+", default=DEFAULT_SCOPES)
parser.add_argument("--wait-timeout", type=int, default=300)
parser.add_argument("--dry-run", action="store_true",
help="Create task but do not wait / update secrets")
args = parser.parse_args()
log("=" * 60)
log("WEVIA PAT RENEW START")
log(f"token_name={args.token_name} expiration_days={args.expiration_days} scopes={args.scopes}")
# Prereqs
issues = check_prereqs()
if issues:
for i in issues:
log(f"PREREQ: {i}", "ERROR")
sys.exit(1)
# Create blade task
task_id, task_path = create_blade_task(
args.token_name,
args.expiration_days,
args.scopes,
)
log(f"Task created: {task_id} at {task_path}")
if args.dry_run:
log("DRY-RUN: task queued, exiting (not waiting for Blade)")
sys.exit(0)
# Wait for Blade result
result = wait_for_result(task_id, args.wait_timeout)
if result is None:
log(f"TIMEOUT after {args.wait_timeout}s - Blade did not produce result", "ERROR")
sys.exit(2)
if not result.get("ok"):
log(f"Blade reported failure: {result.get('error', 'unknown')}", "ERROR")
sys.exit(3)
new_token = result.get("token")
if not new_token or not (new_token.startswith("ghp_") or new_token.startswith("github_pat_")):
log(f"Invalid token format in result: {str(new_token)[:20]}...", "ERROR")
sys.exit(4)
# Update secrets (best effort)
if update_secrets_env(new_token):
log("SUCCESS: PAT renewed and secrets.env updated")
else:
log("PARTIAL: PAT renewed (see blade-tasks JSON) but secrets.env not updated", "WARN")
log("=" * 60)
if __name__ == "__main__":
main()