|
|
""" |
|
|
ALTYZEN Ghost Runner - Complete Stealth Browser Agent |
|
|
====================================================== |
|
|
Zero Docker-level VNC. All binaries download at runtime. |
|
|
Screenshot-based streaming via /stream endpoint. |
|
|
Full validation logic with Nemotron + Gemini fallback. |
|
|
""" |
|
|
|
|
|
import os |
|
|
import sys |
|
|
import asyncio |
|
|
import subprocess |
|
|
import json |
|
|
import logging |
|
|
import threading |
|
|
from datetime import datetime |
|
|
from typing import Dict, Any, Optional |
|
|
from pathlib import Path |
|
|
from contextlib import asynccontextmanager |
|
|
|
|
|
from fastapi import FastAPI, HTTPException |
|
|
from fastapi.responses import HTMLResponse, FileResponse, Response |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
from pydantic import BaseModel |
|
|
import uvicorn |
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
|
|
logger = logging.getLogger("GhostRunner") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RUNTIME_DIR = Path("/tmp/ghost_runner") |
|
|
SCREENSHOT_PATH = RUNTIME_DIR / "screenshot.png" |
|
|
CLOUDFLARED_PATH = RUNTIME_DIR / "cloudflared" |
|
|
LOGS_PATH = RUNTIME_DIR / "agent_logs.txt" |
|
|
|
|
|
|
|
|
class GhostState: |
|
|
browser_ready = False |
|
|
tunnel_process = None |
|
|
current_task = None |
|
|
logs = [] |
|
|
|
|
|
state = GhostState() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TaskRequest(BaseModel): |
|
|
task_id: str |
|
|
task_type: str |
|
|
data: Dict[str, Any] |
|
|
|
|
|
class TaskResponse(BaseModel): |
|
|
task_id: str |
|
|
status: str |
|
|
result: Optional[Dict[str, Any]] = None |
|
|
error: Optional[str] = None |
|
|
logs: Optional[list] = None |
|
|
execution_time_ms: int = 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def setup_runtime(): |
|
|
"""Download all binaries at runtime - invisible to HF build scanners.""" |
|
|
RUNTIME_DIR.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
logger.info("π§ Ghost Runner: Setting up runtime environment...") |
|
|
|
|
|
|
|
|
try: |
|
|
logger.info("π¦ Installing browser engine (this may take a minute)...") |
|
|
proc = await asyncio.create_subprocess_exec( |
|
|
sys.executable, "-m", "playwright", "install", "chromium", |
|
|
stdout=asyncio.subprocess.PIPE, |
|
|
stderr=asyncio.subprocess.PIPE |
|
|
) |
|
|
stdout, stderr = await proc.communicate() |
|
|
if proc.returncode == 0: |
|
|
logger.info("β
Browser engine installed!") |
|
|
state.browser_ready = True |
|
|
else: |
|
|
logger.error(f"β Browser install failed: {stderr.decode()[:200]}") |
|
|
except Exception as e: |
|
|
logger.error(f"β Browser install error: {e}") |
|
|
|
|
|
|
|
|
tunnel_token = os.getenv("CLOUDFLARE_TUNNEL_TOKEN") |
|
|
if tunnel_token and not CLOUDFLARED_PATH.exists(): |
|
|
logger.info("π₯ Downloading cloudflared...") |
|
|
try: |
|
|
proc = await asyncio.create_subprocess_exec( |
|
|
"curl", "-L", "-o", str(CLOUDFLARED_PATH), |
|
|
"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64", |
|
|
stdout=asyncio.subprocess.PIPE, |
|
|
stderr=asyncio.subprocess.PIPE |
|
|
) |
|
|
await proc.communicate() |
|
|
os.chmod(CLOUDFLARED_PATH, 0o755) |
|
|
logger.info("β
cloudflared downloaded!") |
|
|
except Exception as e: |
|
|
logger.error(f"β cloudflared download failed: {e}") |
|
|
|
|
|
|
|
|
if tunnel_token and CLOUDFLARED_PATH.exists(): |
|
|
start_tunnel(tunnel_token) |
|
|
|
|
|
|
|
|
create_placeholder() |
|
|
|
|
|
logger.info("β
Ghost Runner: Runtime environment ready!") |
|
|
|
|
|
|
|
|
def start_tunnel(token: str): |
|
|
"""Start cloudflared tunnel in background.""" |
|
|
def run(): |
|
|
try: |
|
|
logger.info("π Starting Cloudflare tunnel...") |
|
|
state.tunnel_process = subprocess.Popen( |
|
|
[str(CLOUDFLARED_PATH), "tunnel", "--no-autoupdate", "run", |
|
|
"--token", token, "--url", "http://localhost:7860"], |
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE |
|
|
) |
|
|
logger.info("β
Tunnel process started!") |
|
|
except Exception as e: |
|
|
logger.error(f"β Tunnel failed: {e}") |
|
|
|
|
|
threading.Thread(target=run, daemon=True).start() |
|
|
|
|
|
|
|
|
def create_placeholder(): |
|
|
"""Create placeholder screenshot.""" |
|
|
try: |
|
|
from PIL import Image, ImageDraw |
|
|
img = Image.new('RGB', (1280, 720), '#0d1117') |
|
|
draw = ImageDraw.Draw(img) |
|
|
draw.rectangle([100, 100, 1180, 620], outline='#00ff88', width=2) |
|
|
draw.text((480, 320), "ALTYZEN GHOST RUNNER", fill='#00ff88') |
|
|
draw.text((520, 360), "Ready for tasks...", fill='#8b949e') |
|
|
draw.text((490, 420), f"Browser: {'READY' if state.browser_ready else 'LOADING'}", fill='#58a6ff') |
|
|
img.save(SCREENSHOT_PATH) |
|
|
except Exception as e: |
|
|
logger.warning(f"Placeholder creation failed: {e}") |
|
|
|
|
|
SCREENSHOT_PATH.write_bytes(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def validate_order(order_data: Dict[str, Any]) -> Dict[str, Any]: |
|
|
"""Full validation with Nemotron primary + Gemini fallback.""" |
|
|
logs = [] |
|
|
def log(msg): |
|
|
logger.info(msg) |
|
|
logs.append(msg) |
|
|
state.logs.append(msg) |
|
|
|
|
|
if not state.browser_ready: |
|
|
return {"decision": "ERROR", "error": "Browser not ready", "logs": logs} |
|
|
|
|
|
|
|
|
email = order_data.get('email', '') |
|
|
phone = order_data.get('phone', '') |
|
|
zip_code = order_data.get('zip', '') |
|
|
city = order_data.get('city', '') |
|
|
state_name = order_data.get('state', '') |
|
|
task_id = order_data.get('task_id', 'unknown') |
|
|
|
|
|
log(f"π Task ID: {task_id}") |
|
|
log(f"π§ Email: {email}") |
|
|
log(f"π Phone: {phone}") |
|
|
log(f"π Geo: {zip_code}, {city}, {state_name}") |
|
|
|
|
|
|
|
|
task = f""" |
|
|
You are a Validation Expert. Perform these 3 checks: |
|
|
|
|
|
STEP 1: EMAIL VALIDATION |
|
|
- Go to 'https://email-checker.net/' |
|
|
- Input '{email}' and check result |
|
|
- Extract: 'Valid', 'Invalid', or 'Risky' |
|
|
|
|
|
STEP 2: PHONE VALIDATION |
|
|
- Validate phone number '{phone}' |
|
|
- Check if it's a valid format for the region |
|
|
|
|
|
STEP 3: GEO VALIDATION |
|
|
- Does ZIP '{zip_code}' belong to City '{city}' in State '{state_name}'? |
|
|
- Return 'Match' or 'Mismatch' |
|
|
|
|
|
OUTPUT JSON ONLY: |
|
|
{{ |
|
|
"email_status": "Valid/Invalid/Risky", |
|
|
"phone_status": "Valid/Invalid", |
|
|
"geo_match": true/false, |
|
|
"summary": "brief explanation" |
|
|
}} |
|
|
""" |
|
|
|
|
|
api_key = os.getenv("OPENROUTER_API_KEY") |
|
|
if not api_key: |
|
|
return {"decision": "ERROR", "error": "No OPENROUTER_API_KEY", "logs": logs} |
|
|
|
|
|
os.environ["OPENAI_API_KEY"] = api_key |
|
|
|
|
|
try: |
|
|
from browser_use import Agent, Browser |
|
|
from langchain_openai import ChatOpenAI |
|
|
|
|
|
|
|
|
llm_primary = ChatOpenAI( |
|
|
model="nvidia/nemotron-nano-12b-v2-vl:free", |
|
|
api_key=api_key, |
|
|
base_url="https://openrouter.ai/api/v1", |
|
|
temperature=0.1, |
|
|
default_headers={"HTTP-Referer": "https://altyzen.com", "X-Title": "Altyzen Ghost Runner"} |
|
|
) |
|
|
|
|
|
|
|
|
llm_fallback = ChatOpenAI( |
|
|
model="google/gemini-2.0-flash-exp:free", |
|
|
api_key=api_key, |
|
|
base_url="https://openrouter.ai/api/v1", |
|
|
temperature=0.1, |
|
|
default_headers={"HTTP-Referer": "https://altyzen.com", "X-Title": "Altyzen Ghost Runner"} |
|
|
) |
|
|
|
|
|
browser = Browser(headless=True) |
|
|
result = None |
|
|
|
|
|
|
|
|
screenshot_task = asyncio.create_task(capture_screenshots(browser)) |
|
|
|
|
|
try: |
|
|
|
|
|
log("π€ Attempt 1: Using Nvidia Nemotron...") |
|
|
try: |
|
|
agent = Agent(task=task, llm=llm_primary, browser=browser, use_vision=True, validate_output=False) |
|
|
history = await agent.run() |
|
|
result = history.final_result() |
|
|
log("β
Nemotron completed!") |
|
|
except Exception as e: |
|
|
log(f"β οΈ Nemotron failed: {str(e)[:100]}") |
|
|
|
|
|
|
|
|
log("π Switching to Gemini fallback...") |
|
|
try: |
|
|
agent = Agent(task=task, llm=llm_fallback, browser=browser, use_vision=True, validate_output=False) |
|
|
history = await agent.run() |
|
|
result = history.final_result() |
|
|
log("β
Gemini completed!") |
|
|
except Exception as e2: |
|
|
log(f"β Gemini also failed: {str(e2)[:100]}") |
|
|
result = None |
|
|
finally: |
|
|
screenshot_task.cancel() |
|
|
try: |
|
|
await browser.close() |
|
|
except: |
|
|
pass |
|
|
|
|
|
parsed = parse_result(result, order_data) |
|
|
parsed["logs"] = logs |
|
|
parsed["task_id"] = task_id |
|
|
return parsed |
|
|
|
|
|
except Exception as e: |
|
|
log(f"β Critical error: {e}") |
|
|
return {"decision": "ERROR", "error": str(e), "logs": logs} |
|
|
|
|
|
|
|
|
async def capture_screenshots(browser): |
|
|
"""Capture browser screenshots during execution.""" |
|
|
while True: |
|
|
try: |
|
|
context = await browser.get_context() |
|
|
pages = context.pages |
|
|
if pages: |
|
|
await pages[0].screenshot(path=str(SCREENSHOT_PATH)) |
|
|
except: |
|
|
pass |
|
|
await asyncio.sleep(0.5) |
|
|
|
|
|
|
|
|
def parse_result(result, order_data): |
|
|
"""Parse agent result into structured validation response.""" |
|
|
if result is None: |
|
|
return {"decision": "UNKNOWN", "email_valid": False, "phone_valid": False, "geo_valid": False, "reasoning": "All models failed"} |
|
|
|
|
|
parsed = {} |
|
|
if isinstance(result, str): |
|
|
try: |
|
|
if "{" in result: |
|
|
json_start = result.find("{") |
|
|
json_end = result.rfind("}") + 1 |
|
|
parsed = json.loads(result[json_start:json_end]) |
|
|
except: |
|
|
parsed = {"raw": result} |
|
|
elif isinstance(result, dict): |
|
|
parsed = result |
|
|
|
|
|
email_valid = "valid" in str(parsed.get("email_status", "")).lower() and "invalid" not in str(parsed.get("email_status", "")).lower() |
|
|
phone_valid = "valid" in str(parsed.get("phone_status", "")).lower() and "invalid" not in str(parsed.get("phone_status", "")).lower() |
|
|
geo_valid = parsed.get("geo_match", False) if isinstance(parsed.get("geo_match"), bool) else str(parsed.get("geo_match", "")).lower() == "true" |
|
|
|
|
|
decision = "APPROVED" if email_valid and phone_valid and geo_valid else "BLOCKED" |
|
|
|
|
|
return { |
|
|
"order_id": order_data.get("order_id", "UNKNOWN"), |
|
|
"decision": decision, |
|
|
"email_valid": email_valid, |
|
|
"phone_valid": phone_valid, |
|
|
"geo_valid": geo_valid, |
|
|
"reasoning": parsed.get("summary", "Validation completed"), |
|
|
"raw_result": parsed |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager |
|
|
async def lifespan(app: FastAPI): |
|
|
|
|
|
asyncio.create_task(setup_runtime()) |
|
|
yield |
|
|
|
|
|
if state.tunnel_process: |
|
|
state.tunnel_process.terminate() |
|
|
|
|
|
app = FastAPI(title="AI Agent Worker", version="3.0.0", lifespan=lifespan) |
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=["*"], |
|
|
allow_credentials=True, |
|
|
allow_methods=["*"], |
|
|
allow_headers=["*"], |
|
|
) |
|
|
|
|
|
|
|
|
@app.get("/") |
|
|
async def root(): |
|
|
return { |
|
|
"service": "AI Agent Worker", |
|
|
"status": "active", |
|
|
"browser_ready": state.browser_ready, |
|
|
"tunnel_active": state.tunnel_process is not None, |
|
|
"timestamp": datetime.now().isoformat() |
|
|
} |
|
|
|
|
|
|
|
|
@app.get("/health") |
|
|
async def health(): |
|
|
return {"status": "healthy", "browser_ready": state.browser_ready} |
|
|
|
|
|
|
|
|
@app.get("/stream", response_class=HTMLResponse) |
|
|
async def stream(): |
|
|
"""Live screenshot stream - replaces VNC.""" |
|
|
return """ |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>Ghost Runner - Live Feed</title> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
|
<style> |
|
|
* { margin: 0; padding: 0; box-sizing: border-box; } |
|
|
body { |
|
|
background: linear-gradient(135deg, #0d1117 0%, #161b22 100%); |
|
|
min-height: 100vh; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
padding: 20px; |
|
|
font-family: 'Segoe UI', system-ui, sans-serif; |
|
|
} |
|
|
.header { |
|
|
color: #00ff88; |
|
|
font-size: 1.5em; |
|
|
margin-bottom: 15px; |
|
|
text-shadow: 0 0 20px rgba(0,255,136,0.3); |
|
|
} |
|
|
.container { |
|
|
position: relative; |
|
|
border: 2px solid #30363d; |
|
|
border-radius: 12px; |
|
|
overflow: hidden; |
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4); |
|
|
} |
|
|
img { |
|
|
display: block; |
|
|
max-width: 100%; |
|
|
width: 1280px; |
|
|
} |
|
|
.status-bar { |
|
|
position: absolute; |
|
|
bottom: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
background: rgba(0,0,0,0.8); |
|
|
padding: 8px 15px; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
color: #8b949e; |
|
|
font-size: 12px; |
|
|
} |
|
|
.live-dot { |
|
|
display: inline-block; |
|
|
width: 8px; |
|
|
height: 8px; |
|
|
background: #00ff88; |
|
|
border-radius: 50%; |
|
|
margin-right: 5px; |
|
|
animation: pulse 1s infinite; |
|
|
} |
|
|
@keyframes pulse { |
|
|
0%, 100% { opacity: 1; } |
|
|
50% { opacity: 0.3; } |
|
|
} |
|
|
.logs { |
|
|
margin-top: 20px; |
|
|
width: 100%; |
|
|
max-width: 1280px; |
|
|
background: #161b22; |
|
|
border: 1px solid #30363d; |
|
|
border-radius: 8px; |
|
|
padding: 15px; |
|
|
color: #c9d1d9; |
|
|
font-family: monospace; |
|
|
font-size: 12px; |
|
|
max-height: 200px; |
|
|
overflow-y: auto; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="header">π» ALTYZEN Ghost Runner - Live Feed</div> |
|
|
<div class="container"> |
|
|
<img id="screen" src="/screenshot" alt="Browser Feed"/> |
|
|
<div class="status-bar"> |
|
|
<span><span class="live-dot"></span>LIVE</span> |
|
|
<span id="timestamp">Connecting...</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="logs" id="logs">Waiting for activity...</div> |
|
|
<script> |
|
|
setInterval(() => { |
|
|
document.getElementById('screen').src = '/screenshot?' + Date.now(); |
|
|
document.getElementById('timestamp').textContent = new Date().toLocaleTimeString(); |
|
|
}, 1000); |
|
|
|
|
|
setInterval(async () => { |
|
|
try { |
|
|
const resp = await fetch('/logs'); |
|
|
const data = await resp.json(); |
|
|
if (data.logs && data.logs.length > 0) { |
|
|
document.getElementById('logs').innerHTML = data.logs.slice(-20).map(l => `<div>${l}</div>`).join(''); |
|
|
} |
|
|
} catch (e) {} |
|
|
}, 2000); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
|
|
|
@app.get("/screenshot") |
|
|
async def screenshot(): |
|
|
"""Current browser screenshot.""" |
|
|
if SCREENSHOT_PATH.exists(): |
|
|
return FileResponse(SCREENSHOT_PATH, media_type="image/png") |
|
|
create_placeholder() |
|
|
return FileResponse(SCREENSHOT_PATH, media_type="image/png") |
|
|
|
|
|
|
|
|
@app.get("/logs") |
|
|
async def get_logs(): |
|
|
"""Return recent logs.""" |
|
|
return {"logs": state.logs[-50:]} |
|
|
|
|
|
|
|
|
@app.post("/run-task", response_model=TaskResponse) |
|
|
async def run_task(request: TaskRequest): |
|
|
"""Run a validation task.""" |
|
|
start_time = datetime.now() |
|
|
state.current_task = request.task_id |
|
|
state.logs = [] |
|
|
|
|
|
try: |
|
|
if request.task_type in ["validate_order", "validate_email"]: |
|
|
result = await validate_order({**request.data, "task_id": request.task_id}) |
|
|
execution_time = int((datetime.now() - start_time).total_seconds() * 1000) |
|
|
|
|
|
return TaskResponse( |
|
|
task_id=request.task_id, |
|
|
status="success" if result.get("decision") not in ["ERROR", "UNKNOWN"] else "failed", |
|
|
result=result, |
|
|
logs=result.get("logs", []), |
|
|
execution_time_ms=execution_time |
|
|
) |
|
|
else: |
|
|
raise HTTPException(status_code=400, detail=f"Unknown task_type: {request.task_type}") |
|
|
|
|
|
except Exception as e: |
|
|
return TaskResponse(task_id=request.task_id, status="error", error=str(e)) |
|
|
finally: |
|
|
state.current_task = None |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
uvicorn.run(app, host="0.0.0.0", port=7860) |
|
|
|