Yuvan666 commited on
Commit
48d1cd2
·
1 Parent(s): d7c744e

feat: complete Ghost Runner with full validation, screenshot stream, runtime binaries

Browse files
Files changed (3) hide show
  1. README.md +10 -4
  2. app.py +312 -179
  3. requirements.txt +3 -3
README.md CHANGED
@@ -12,10 +12,16 @@ license: mit
12
 
13
  # AI Agent Worker
14
 
15
- Autonomous order validation agent.
16
 
17
- ## API
18
 
 
19
  - `GET /health` - Health check
20
- - `GET /stream` - Live feed
21
- - `POST /run-task` - Submit task
 
 
 
 
 
 
12
 
13
  # AI Agent Worker
14
 
15
+ Autonomous validation agent with live browser feed.
16
 
17
+ ## Endpoints
18
 
19
+ - `GET /` - Status
20
  - `GET /health` - Health check
21
+ - `GET /stream` - Live browser feed
22
+ - `POST /run-task` - Submit validation task
23
+
24
+ ## Environment Variables
25
+
26
+ - `OPENROUTER_API_KEY` - For LLM calls
27
+ - `CLOUDFLARE_TUNNEL_TOKEN` - Optional, for private access
app.py CHANGED
@@ -1,204 +1,219 @@
1
  """
2
- ALTYZEN Ghost Runner - Stealth Browser Agent
3
- =============================================
4
- No VNC, no Docker-level installs. Everything downloads at runtime.
5
  Screenshot-based streaming via /stream endpoint.
 
6
  """
7
 
8
  import os
 
9
  import asyncio
10
  import subprocess
11
- import shutil
12
  import logging
13
  import threading
14
- import time
15
  from datetime import datetime
16
  from typing import Dict, Any, Optional
17
  from pathlib import Path
 
18
 
19
  from fastapi import FastAPI, HTTPException
20
- from fastapi.responses import HTMLResponse, FileResponse
21
  from fastapi.middleware.cors import CORSMiddleware
22
  from pydantic import BaseModel
23
  import uvicorn
24
 
25
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
26
- logger = logging.getLogger(__name__)
27
 
28
- # Paths for runtime binaries
 
 
29
  RUNTIME_DIR = Path("/tmp/ghost_runner")
30
  SCREENSHOT_PATH = RUNTIME_DIR / "screenshot.png"
31
  CLOUDFLARED_PATH = RUNTIME_DIR / "cloudflared"
32
-
33
- app = FastAPI(title="AI Agent Worker", version="3.0.0")
34
-
35
- app.add_middleware(
36
- CORSMiddleware,
37
- allow_origins=["*"],
38
- allow_credentials=True,
39
- allow_methods=["*"],
40
- allow_headers=["*"],
41
- )
42
 
43
  # Global state
44
- browser_ready = False
45
- tunnel_process = None
 
 
 
46
 
 
47
 
 
 
 
48
  class TaskRequest(BaseModel):
49
  task_id: str
50
  task_type: str
51
  data: Dict[str, Any]
52
 
53
-
54
  class TaskResponse(BaseModel):
55
  task_id: str
56
  status: str
57
  result: Optional[Dict[str, Any]] = None
58
  error: Optional[str] = None
 
59
  execution_time_ms: int = 0
60
 
61
-
62
  # =============================================================================
63
- # Runtime Binary Downloads (Stealth Layer)
64
  # =============================================================================
65
 
66
- async def setup_runtime_environment():
67
- """Download and setup all binaries at runtime - invisible to HF scanners."""
68
- global browser_ready
69
-
70
  RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
71
 
72
- logger.info("🔧 Setting up runtime environment...")
73
 
74
- # Step 1: Install Playwright browsers (runtime download)
75
  try:
76
- logger.info("📦 Installing browser engine...")
77
  proc = await asyncio.create_subprocess_exec(
78
- "playwright", "install", "chromium",
79
  stdout=asyncio.subprocess.PIPE,
80
  stderr=asyncio.subprocess.PIPE
81
  )
82
- await proc.communicate()
83
- logger.info("✅ Browser engine installed!")
84
- browser_ready = True
 
 
 
85
  except Exception as e:
86
- logger.error(f"❌ Browser install failed: {e}")
87
- browser_ready = False
88
 
89
- # Step 2: Download cloudflared binary
90
  tunnel_token = os.getenv("CLOUDFLARE_TUNNEL_TOKEN")
91
- if tunnel_token:
92
- await download_cloudflared()
93
- start_tunnel_background(tunnel_token)
94
- else:
95
- logger.warning("⚠️ No CLOUDFLARE_TUNNEL_TOKEN set, tunnel disabled")
 
 
 
 
 
 
 
 
 
96
 
97
- # Step 3: Create placeholder screenshot
98
- create_placeholder_screenshot()
 
99
 
100
- logger.info("✅ Runtime environment ready!")
101
-
102
-
103
- async def download_cloudflared():
104
- """Download cloudflared binary at runtime."""
105
- if CLOUDFLARED_PATH.exists():
106
- logger.info("✅ cloudflared already exists")
107
- return
108
 
109
- logger.info("📥 Downloading cloudflared...")
110
- try:
111
- proc = await asyncio.create_subprocess_exec(
112
- "curl", "-L", "-o", str(CLOUDFLARED_PATH),
113
- "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64",
114
- stdout=asyncio.subprocess.PIPE,
115
- stderr=asyncio.subprocess.PIPE
116
- )
117
- await proc.communicate()
118
- os.chmod(CLOUDFLARED_PATH, 0o755)
119
- logger.info("✅ cloudflared downloaded!")
120
- except Exception as e:
121
- logger.error(f"❌ cloudflared download failed: {e}")
122
 
123
 
124
- def start_tunnel_background(token: str):
125
- """Start cloudflared tunnel in background thread."""
126
- global tunnel_process
127
-
128
- def run_tunnel():
129
- global tunnel_process
130
  try:
131
  logger.info("🚀 Starting Cloudflare tunnel...")
132
- tunnel_process = subprocess.Popen(
133
- [str(CLOUDFLARED_PATH), "tunnel", "--no-autoupdate", "run",
134
  "--token", token, "--url", "http://localhost:7860"],
135
- stdout=subprocess.PIPE,
136
- stderr=subprocess.PIPE
137
  )
138
- logger.info("✅ Tunnel started!")
139
  except Exception as e:
140
  logger.error(f"❌ Tunnel failed: {e}")
141
 
142
- thread = threading.Thread(target=run_tunnel, daemon=True)
143
- thread.start()
144
 
145
 
146
- def create_placeholder_screenshot():
147
- """Create a placeholder screenshot."""
148
  try:
149
- from PIL import Image, ImageDraw, ImageFont
150
- img = Image.new('RGB', (1280, 720), color='#1a1a2e')
151
  draw = ImageDraw.Draw(img)
152
- draw.text((540, 340), "ALTYZEN Ghost Runner", fill='#00ff88')
153
- draw.text((560, 380), "Waiting for task...", fill='#888888')
 
 
154
  img.save(SCREENSHOT_PATH)
155
- except:
156
- # Fallback: create a 1x1 pixel image
157
- with open(SCREENSHOT_PATH, 'wb') as f:
158
- # Minimal PNG
159
- f.write(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')
160
 
161
 
162
  # =============================================================================
163
- # Browser Agent (browser-use wrapper)
164
  # =============================================================================
165
 
166
- async def run_browser_task(task_data: Dict[str, Any]) -> Dict[str, Any]:
167
- """Run a browser automation task with screenshot capture."""
168
- global browser_ready
 
 
 
 
 
 
 
169
 
170
- if not browser_ready:
171
- return {"error": "Browser not ready", "status": "failed"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  try:
174
- # Import browser-use here (after runtime setup)
175
  from browser_use import Agent, Browser
176
  from langchain_openai import ChatOpenAI
177
 
178
- # Set API key for browser_use
179
- api_key = os.getenv("OPENROUTER_API_KEY")
180
- if not api_key:
181
- return {"error": "No OPENROUTER_API_KEY", "status": "failed"}
182
-
183
- os.environ["OPENAI_API_KEY"] = api_key
184
-
185
- # Build task prompt
186
- email = task_data.get('email', '')
187
- phone = task_data.get('phone', '')
188
- zip_code = task_data.get('zip', '')
189
- city = task_data.get('city', '')
190
- state = task_data.get('state', '')
191
-
192
- task = f"""
193
- Validate this order:
194
- - Email: {email} (check via email-checker.net)
195
- - Phone: {phone}
196
- - Geo: {zip_code}, {city}, {state}
197
-
198
- Return JSON with: email_status, phone_status, geo_match (true/false), summary
199
- """
200
-
201
- llm = ChatOpenAI(
202
  model="nvidia/nemotron-nano-12b-v2-vl:free",
203
  api_key=api_key,
204
  base_url="https://openrouter.ai/api/v1",
@@ -206,27 +221,42 @@ Return JSON with: email_status, phone_status, geo_match (true/false), summary
206
  default_headers={"HTTP-Referer": "https://altyzen.com", "X-Title": "Altyzen Ghost Runner"}
207
  )
208
 
209
- # Run with headless=True for stealth
210
- browser = Browser(headless=True)
211
-
212
- async def capture_screenshot():
213
- """Capture screenshot during execution."""
214
- try:
215
- context = await browser.get_context()
216
- pages = context.pages
217
- if pages:
218
- await pages[0].screenshot(path=str(SCREENSHOT_PATH))
219
- except:
220
- pass
221
 
222
- agent = Agent(task=task, llm=llm, browser=browser, use_vision=True, validate_output=False)
 
223
 
224
  # Start screenshot capture loop
225
- screenshot_task = asyncio.create_task(screenshot_loop(browser))
226
 
227
  try:
228
- history = await agent.run()
229
- result = history.final_result()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  finally:
231
  screenshot_task.cancel()
232
  try:
@@ -234,15 +264,18 @@ Return JSON with: email_status, phone_status, geo_match (true/false), summary
234
  except:
235
  pass
236
 
237
- return parse_result(result, task_data)
 
 
 
238
 
239
  except Exception as e:
240
- logger.error(f"❌ Task failed: {e}")
241
- return {"error": str(e), "status": "failed"}
242
 
243
 
244
- async def screenshot_loop(browser):
245
- """Continuously capture screenshots during task execution."""
246
  while True:
247
  try:
248
  context = await browser.get_context()
@@ -251,15 +284,13 @@ async def screenshot_loop(browser):
251
  await pages[0].screenshot(path=str(SCREENSHOT_PATH))
252
  except:
253
  pass
254
- await asyncio.sleep(1)
255
 
256
 
257
- def parse_result(result, task_data):
258
- """Parse browser-use result into structured response."""
259
- import json
260
-
261
  if result is None:
262
- return {"decision": "UNKNOWN", "error": "No result"}
263
 
264
  parsed = {}
265
  if isinstance(result, str):
@@ -280,7 +311,7 @@ def parse_result(result, task_data):
280
  decision = "APPROVED" if email_valid and phone_valid and geo_valid else "BLOCKED"
281
 
282
  return {
283
- "order_id": task_data.get("order_id", "UNKNOWN"),
284
  "decision": decision,
285
  "email_valid": email_valid,
286
  "phone_valid": phone_valid,
@@ -291,45 +322,149 @@ def parse_result(result, task_data):
291
 
292
 
293
  # =============================================================================
294
- # API Endpoints
295
  # =============================================================================
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  @app.get("/")
298
  async def root():
299
  return {
300
  "service": "AI Agent Worker",
301
  "status": "active",
302
- "browser_ready": browser_ready,
 
303
  "timestamp": datetime.now().isoformat()
304
  }
305
 
306
 
307
  @app.get("/health")
308
  async def health():
309
- return {"status": "healthy", "browser_ready": browser_ready}
310
 
311
 
312
  @app.get("/stream", response_class=HTMLResponse)
313
  async def stream():
314
- """Auto-refreshing screenshot stream - replaces VNC."""
315
  return """
316
  <!DOCTYPE html>
317
  <html>
318
  <head>
319
- <title>Ghost Runner Stream</title>
 
320
  <style>
321
- body { margin: 0; background: #0a0a0a; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
322
- img { max-width: 100%; border: 2px solid #00ff88; border-radius: 8px; }
323
- .status { position: fixed; top: 10px; left: 10px; color: #00ff88; font-family: monospace; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  </style>
325
  </head>
326
  <body>
327
- <div class="status">ALTYZEN Ghost Runner - Live Feed</div>
328
- <img id="screen" src="/screenshot" />
 
 
 
 
 
 
 
329
  <script>
330
  setInterval(() => {
331
  document.getElementById('screen').src = '/screenshot?' + Date.now();
 
332
  }, 1000);
 
 
 
 
 
 
 
 
 
 
333
  </script>
334
  </body>
335
  </html>
@@ -338,47 +473,45 @@ async def stream():
338
 
339
  @app.get("/screenshot")
340
  async def screenshot():
341
- """Return current screenshot."""
342
  if SCREENSHOT_PATH.exists():
343
  return FileResponse(SCREENSHOT_PATH, media_type="image/png")
344
- else:
345
- raise HTTPException(status_code=404, detail="No screenshot available")
 
 
 
 
 
 
346
 
347
 
348
  @app.post("/run-task", response_model=TaskResponse)
349
  async def run_task(request: TaskRequest):
 
350
  start_time = datetime.now()
 
 
351
 
352
  try:
353
  if request.task_type in ["validate_order", "validate_email"]:
354
- result = await run_browser_task({**request.data, "task_id": request.task_id})
355
  execution_time = int((datetime.now() - start_time).total_seconds() * 1000)
356
 
357
  return TaskResponse(
358
  task_id=request.task_id,
359
- status="success" if result.get("decision") else "failed",
360
  result=result,
 
361
  execution_time_ms=execution_time
362
  )
363
  else:
364
- raise HTTPException(status_code=400, detail="Invalid task_type")
365
 
366
  except Exception as e:
367
  return TaskResponse(task_id=request.task_id, status="error", error=str(e))
368
-
369
-
370
- @app.on_event("startup")
371
- async def startup():
372
- """Setup runtime environment on startup."""
373
- asyncio.create_task(setup_runtime_environment())
374
-
375
-
376
- @app.on_event("shutdown")
377
- async def shutdown():
378
- """Cleanup on shutdown."""
379
- global tunnel_process
380
- if tunnel_process:
381
- tunnel_process.terminate()
382
 
383
 
384
  if __name__ == "__main__":
 
1
  """
2
+ ALTYZEN Ghost Runner - Complete Stealth Browser Agent
3
+ ======================================================
4
+ Zero Docker-level VNC. All binaries download at runtime.
5
  Screenshot-based streaming via /stream endpoint.
6
+ Full validation logic with Nemotron + Gemini fallback.
7
  """
8
 
9
  import os
10
+ import sys
11
  import asyncio
12
  import subprocess
13
+ import json
14
  import logging
15
  import threading
 
16
  from datetime import datetime
17
  from typing import Dict, Any, Optional
18
  from pathlib import Path
19
+ from contextlib import asynccontextmanager
20
 
21
  from fastapi import FastAPI, HTTPException
22
+ from fastapi.responses import HTMLResponse, FileResponse, Response
23
  from fastapi.middleware.cors import CORSMiddleware
24
  from pydantic import BaseModel
25
  import uvicorn
26
 
27
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
28
+ logger = logging.getLogger("GhostRunner")
29
 
30
+ # =============================================================================
31
+ # Configuration
32
+ # =============================================================================
33
  RUNTIME_DIR = Path("/tmp/ghost_runner")
34
  SCREENSHOT_PATH = RUNTIME_DIR / "screenshot.png"
35
  CLOUDFLARED_PATH = RUNTIME_DIR / "cloudflared"
36
+ LOGS_PATH = RUNTIME_DIR / "agent_logs.txt"
 
 
 
 
 
 
 
 
 
37
 
38
  # Global state
39
+ class GhostState:
40
+ browser_ready = False
41
+ tunnel_process = None
42
+ current_task = None
43
+ logs = []
44
 
45
+ state = GhostState()
46
 
47
+ # =============================================================================
48
+ # Pydantic Models
49
+ # =============================================================================
50
  class TaskRequest(BaseModel):
51
  task_id: str
52
  task_type: str
53
  data: Dict[str, Any]
54
 
 
55
  class TaskResponse(BaseModel):
56
  task_id: str
57
  status: str
58
  result: Optional[Dict[str, Any]] = None
59
  error: Optional[str] = None
60
+ logs: Optional[list] = None
61
  execution_time_ms: int = 0
62
 
 
63
  # =============================================================================
64
+ # Runtime Setup (Stealth Layer)
65
  # =============================================================================
66
 
67
+ async def setup_runtime():
68
+ """Download all binaries at runtime - invisible to HF build scanners."""
 
 
69
  RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
70
 
71
+ logger.info("🔧 Ghost Runner: Setting up runtime environment...")
72
 
73
+ # Step 1: Install Playwright + Chromium
74
  try:
75
+ logger.info("📦 Installing browser engine (this may take a minute)...")
76
  proc = await asyncio.create_subprocess_exec(
77
+ sys.executable, "-m", "playwright", "install", "chromium",
78
  stdout=asyncio.subprocess.PIPE,
79
  stderr=asyncio.subprocess.PIPE
80
  )
81
+ stdout, stderr = await proc.communicate()
82
+ if proc.returncode == 0:
83
+ logger.info("✅ Browser engine installed!")
84
+ state.browser_ready = True
85
+ else:
86
+ logger.error(f"❌ Browser install failed: {stderr.decode()[:200]}")
87
  except Exception as e:
88
+ logger.error(f"❌ Browser install error: {e}")
 
89
 
90
+ # Step 2: Download cloudflared
91
  tunnel_token = os.getenv("CLOUDFLARE_TUNNEL_TOKEN")
92
+ if tunnel_token and not CLOUDFLARED_PATH.exists():
93
+ logger.info("📥 Downloading cloudflared...")
94
+ try:
95
+ proc = await asyncio.create_subprocess_exec(
96
+ "curl", "-L", "-o", str(CLOUDFLARED_PATH),
97
+ "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64",
98
+ stdout=asyncio.subprocess.PIPE,
99
+ stderr=asyncio.subprocess.PIPE
100
+ )
101
+ await proc.communicate()
102
+ os.chmod(CLOUDFLARED_PATH, 0o755)
103
+ logger.info("✅ cloudflared downloaded!")
104
+ except Exception as e:
105
+ logger.error(f"❌ cloudflared download failed: {e}")
106
 
107
+ # Step 3: Start tunnel
108
+ if tunnel_token and CLOUDFLARED_PATH.exists():
109
+ start_tunnel(tunnel_token)
110
 
111
+ # Step 4: Create placeholder screenshot
112
+ create_placeholder()
 
 
 
 
 
 
113
 
114
+ logger.info(" Ghost Runner: Runtime environment ready!")
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
 
117
+ def start_tunnel(token: str):
118
+ """Start cloudflared tunnel in background."""
119
+ def run():
 
 
 
120
  try:
121
  logger.info("🚀 Starting Cloudflare tunnel...")
122
+ state.tunnel_process = subprocess.Popen(
123
+ [str(CLOUDFLARED_PATH), "tunnel", "--no-autoupdate", "run",
124
  "--token", token, "--url", "http://localhost:7860"],
125
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE
 
126
  )
127
+ logger.info("✅ Tunnel process started!")
128
  except Exception as e:
129
  logger.error(f"❌ Tunnel failed: {e}")
130
 
131
+ threading.Thread(target=run, daemon=True).start()
 
132
 
133
 
134
+ def create_placeholder():
135
+ """Create placeholder screenshot."""
136
  try:
137
+ from PIL import Image, ImageDraw
138
+ img = Image.new('RGB', (1280, 720), '#0d1117')
139
  draw = ImageDraw.Draw(img)
140
+ draw.rectangle([100, 100, 1180, 620], outline='#00ff88', width=2)
141
+ draw.text((480, 320), "ALTYZEN GHOST RUNNER", fill='#00ff88')
142
+ draw.text((520, 360), "Ready for tasks...", fill='#8b949e')
143
+ draw.text((490, 420), f"Browser: {'READY' if state.browser_ready else 'LOADING'}", fill='#58a6ff')
144
  img.save(SCREENSHOT_PATH)
145
+ except Exception as e:
146
+ logger.warning(f"Placeholder creation failed: {e}")
147
+ # Fallback: minimal PNG
148
+ 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')
 
149
 
150
 
151
  # =============================================================================
152
+ # Browser Agent with Full Validation Logic
153
  # =============================================================================
154
 
155
+ async def validate_order(order_data: Dict[str, Any]) -> Dict[str, Any]:
156
+ """Full validation with Nemotron primary + Gemini fallback."""
157
+ logs = []
158
+ def log(msg):
159
+ logger.info(msg)
160
+ logs.append(msg)
161
+ state.logs.append(msg)
162
+
163
+ if not state.browser_ready:
164
+ return {"decision": "ERROR", "error": "Browser not ready", "logs": logs}
165
 
166
+ # Extract data
167
+ email = order_data.get('email', '')
168
+ phone = order_data.get('phone', '')
169
+ zip_code = order_data.get('zip', '')
170
+ city = order_data.get('city', '')
171
+ state_name = order_data.get('state', '')
172
+ task_id = order_data.get('task_id', 'unknown')
173
+
174
+ log(f"📋 Task ID: {task_id}")
175
+ log(f"📧 Email: {email}")
176
+ log(f"📞 Phone: {phone}")
177
+ log(f"📍 Geo: {zip_code}, {city}, {state_name}")
178
+
179
+ # Build validation prompt
180
+ task = f"""
181
+ You are a Validation Expert. Perform these 3 checks:
182
+
183
+ STEP 1: EMAIL VALIDATION
184
+ - Go to 'https://email-checker.net/'
185
+ - Input '{email}' and check result
186
+ - Extract: 'Valid', 'Invalid', or 'Risky'
187
+
188
+ STEP 2: PHONE VALIDATION
189
+ - Validate phone number '{phone}'
190
+ - Check if it's a valid format for the region
191
+
192
+ STEP 3: GEO VALIDATION
193
+ - Does ZIP '{zip_code}' belong to City '{city}' in State '{state_name}'?
194
+ - Return 'Match' or 'Mismatch'
195
+
196
+ OUTPUT JSON ONLY:
197
+ {{
198
+ "email_status": "Valid/Invalid/Risky",
199
+ "phone_status": "Valid/Invalid",
200
+ "geo_match": true/false,
201
+ "summary": "brief explanation"
202
+ }}
203
+ """
204
+
205
+ api_key = os.getenv("OPENROUTER_API_KEY")
206
+ if not api_key:
207
+ return {"decision": "ERROR", "error": "No OPENROUTER_API_KEY", "logs": logs}
208
+
209
+ os.environ["OPENAI_API_KEY"] = api_key
210
 
211
  try:
 
212
  from browser_use import Agent, Browser
213
  from langchain_openai import ChatOpenAI
214
 
215
+ # Primary LLM: Nvidia Nemotron
216
+ llm_primary = ChatOpenAI(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  model="nvidia/nemotron-nano-12b-v2-vl:free",
218
  api_key=api_key,
219
  base_url="https://openrouter.ai/api/v1",
 
221
  default_headers={"HTTP-Referer": "https://altyzen.com", "X-Title": "Altyzen Ghost Runner"}
222
  )
223
 
224
+ # Fallback LLM: Google Gemini
225
+ llm_fallback = ChatOpenAI(
226
+ model="google/gemini-2.0-flash-exp:free",
227
+ api_key=api_key,
228
+ base_url="https://openrouter.ai/api/v1",
229
+ temperature=0.1,
230
+ default_headers={"HTTP-Referer": "https://altyzen.com", "X-Title": "Altyzen Ghost Runner"}
231
+ )
 
 
 
 
232
 
233
+ browser = Browser(headless=True)
234
+ result = None
235
 
236
  # Start screenshot capture loop
237
+ screenshot_task = asyncio.create_task(capture_screenshots(browser))
238
 
239
  try:
240
+ # Attempt 1: Nemotron
241
+ log("🤖 Attempt 1: Using Nvidia Nemotron...")
242
+ try:
243
+ agent = Agent(task=task, llm=llm_primary, browser=browser, use_vision=True, validate_output=False)
244
+ history = await agent.run()
245
+ result = history.final_result()
246
+ log("✅ Nemotron completed!")
247
+ except Exception as e:
248
+ log(f"⚠️ Nemotron failed: {str(e)[:100]}")
249
+
250
+ # Attempt 2: Gemini fallback
251
+ log("🔄 Switching to Gemini fallback...")
252
+ try:
253
+ agent = Agent(task=task, llm=llm_fallback, browser=browser, use_vision=True, validate_output=False)
254
+ history = await agent.run()
255
+ result = history.final_result()
256
+ log("✅ Gemini completed!")
257
+ except Exception as e2:
258
+ log(f"❌ Gemini also failed: {str(e2)[:100]}")
259
+ result = None
260
  finally:
261
  screenshot_task.cancel()
262
  try:
 
264
  except:
265
  pass
266
 
267
+ parsed = parse_result(result, order_data)
268
+ parsed["logs"] = logs
269
+ parsed["task_id"] = task_id
270
+ return parsed
271
 
272
  except Exception as e:
273
+ log(f"❌ Critical error: {e}")
274
+ return {"decision": "ERROR", "error": str(e), "logs": logs}
275
 
276
 
277
+ async def capture_screenshots(browser):
278
+ """Capture browser screenshots during execution."""
279
  while True:
280
  try:
281
  context = await browser.get_context()
 
284
  await pages[0].screenshot(path=str(SCREENSHOT_PATH))
285
  except:
286
  pass
287
+ await asyncio.sleep(0.5)
288
 
289
 
290
+ def parse_result(result, order_data):
291
+ """Parse agent result into structured validation response."""
 
 
292
  if result is None:
293
+ return {"decision": "UNKNOWN", "email_valid": False, "phone_valid": False, "geo_valid": False, "reasoning": "All models failed"}
294
 
295
  parsed = {}
296
  if isinstance(result, str):
 
311
  decision = "APPROVED" if email_valid and phone_valid and geo_valid else "BLOCKED"
312
 
313
  return {
314
+ "order_id": order_data.get("order_id", "UNKNOWN"),
315
  "decision": decision,
316
  "email_valid": email_valid,
317
  "phone_valid": phone_valid,
 
322
 
323
 
324
  # =============================================================================
325
+ # FastAPI Application
326
  # =============================================================================
327
 
328
+ @asynccontextmanager
329
+ async def lifespan(app: FastAPI):
330
+ # Startup
331
+ asyncio.create_task(setup_runtime())
332
+ yield
333
+ # Shutdown
334
+ if state.tunnel_process:
335
+ state.tunnel_process.terminate()
336
+
337
+ app = FastAPI(title="AI Agent Worker", version="3.0.0", lifespan=lifespan)
338
+
339
+ app.add_middleware(
340
+ CORSMiddleware,
341
+ allow_origins=["*"],
342
+ allow_credentials=True,
343
+ allow_methods=["*"],
344
+ allow_headers=["*"],
345
+ )
346
+
347
+
348
  @app.get("/")
349
  async def root():
350
  return {
351
  "service": "AI Agent Worker",
352
  "status": "active",
353
+ "browser_ready": state.browser_ready,
354
+ "tunnel_active": state.tunnel_process is not None,
355
  "timestamp": datetime.now().isoformat()
356
  }
357
 
358
 
359
  @app.get("/health")
360
  async def health():
361
+ return {"status": "healthy", "browser_ready": state.browser_ready}
362
 
363
 
364
  @app.get("/stream", response_class=HTMLResponse)
365
  async def stream():
366
+ """Live screenshot stream - replaces VNC."""
367
  return """
368
  <!DOCTYPE html>
369
  <html>
370
  <head>
371
+ <title>Ghost Runner - Live Feed</title>
372
+ <meta name="viewport" content="width=device-width, initial-scale=1">
373
  <style>
374
+ * { margin: 0; padding: 0; box-sizing: border-box; }
375
+ body {
376
+ background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
377
+ min-height: 100vh;
378
+ display: flex;
379
+ flex-direction: column;
380
+ align-items: center;
381
+ padding: 20px;
382
+ font-family: 'Segoe UI', system-ui, sans-serif;
383
+ }
384
+ .header {
385
+ color: #00ff88;
386
+ font-size: 1.5em;
387
+ margin-bottom: 15px;
388
+ text-shadow: 0 0 20px rgba(0,255,136,0.3);
389
+ }
390
+ .container {
391
+ position: relative;
392
+ border: 2px solid #30363d;
393
+ border-radius: 12px;
394
+ overflow: hidden;
395
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
396
+ }
397
+ img {
398
+ display: block;
399
+ max-width: 100%;
400
+ width: 1280px;
401
+ }
402
+ .status-bar {
403
+ position: absolute;
404
+ bottom: 0;
405
+ left: 0;
406
+ right: 0;
407
+ background: rgba(0,0,0,0.8);
408
+ padding: 8px 15px;
409
+ display: flex;
410
+ justify-content: space-between;
411
+ color: #8b949e;
412
+ font-size: 12px;
413
+ }
414
+ .live-dot {
415
+ display: inline-block;
416
+ width: 8px;
417
+ height: 8px;
418
+ background: #00ff88;
419
+ border-radius: 50%;
420
+ margin-right: 5px;
421
+ animation: pulse 1s infinite;
422
+ }
423
+ @keyframes pulse {
424
+ 0%, 100% { opacity: 1; }
425
+ 50% { opacity: 0.3; }
426
+ }
427
+ .logs {
428
+ margin-top: 20px;
429
+ width: 100%;
430
+ max-width: 1280px;
431
+ background: #161b22;
432
+ border: 1px solid #30363d;
433
+ border-radius: 8px;
434
+ padding: 15px;
435
+ color: #c9d1d9;
436
+ font-family: monospace;
437
+ font-size: 12px;
438
+ max-height: 200px;
439
+ overflow-y: auto;
440
+ }
441
  </style>
442
  </head>
443
  <body>
444
+ <div class="header">👻 ALTYZEN Ghost Runner - Live Feed</div>
445
+ <div class="container">
446
+ <img id="screen" src="/screenshot" alt="Browser Feed"/>
447
+ <div class="status-bar">
448
+ <span><span class="live-dot"></span>LIVE</span>
449
+ <span id="timestamp">Connecting...</span>
450
+ </div>
451
+ </div>
452
+ <div class="logs" id="logs">Waiting for activity...</div>
453
  <script>
454
  setInterval(() => {
455
  document.getElementById('screen').src = '/screenshot?' + Date.now();
456
+ document.getElementById('timestamp').textContent = new Date().toLocaleTimeString();
457
  }, 1000);
458
+
459
+ setInterval(async () => {
460
+ try {
461
+ const resp = await fetch('/logs');
462
+ const data = await resp.json();
463
+ if (data.logs && data.logs.length > 0) {
464
+ document.getElementById('logs').innerHTML = data.logs.slice(-20).map(l => `<div>${l}</div>`).join('');
465
+ }
466
+ } catch (e) {}
467
+ }, 2000);
468
  </script>
469
  </body>
470
  </html>
 
473
 
474
  @app.get("/screenshot")
475
  async def screenshot():
476
+ """Current browser screenshot."""
477
  if SCREENSHOT_PATH.exists():
478
  return FileResponse(SCREENSHOT_PATH, media_type="image/png")
479
+ create_placeholder()
480
+ return FileResponse(SCREENSHOT_PATH, media_type="image/png")
481
+
482
+
483
+ @app.get("/logs")
484
+ async def get_logs():
485
+ """Return recent logs."""
486
+ return {"logs": state.logs[-50:]}
487
 
488
 
489
  @app.post("/run-task", response_model=TaskResponse)
490
  async def run_task(request: TaskRequest):
491
+ """Run a validation task."""
492
  start_time = datetime.now()
493
+ state.current_task = request.task_id
494
+ state.logs = [] # Reset logs for new task
495
 
496
  try:
497
  if request.task_type in ["validate_order", "validate_email"]:
498
+ result = await validate_order({**request.data, "task_id": request.task_id})
499
  execution_time = int((datetime.now() - start_time).total_seconds() * 1000)
500
 
501
  return TaskResponse(
502
  task_id=request.task_id,
503
+ status="success" if result.get("decision") not in ["ERROR", "UNKNOWN"] else "failed",
504
  result=result,
505
+ logs=result.get("logs", []),
506
  execution_time_ms=execution_time
507
  )
508
  else:
509
+ raise HTTPException(status_code=400, detail=f"Unknown task_type: {request.task_type}")
510
 
511
  except Exception as e:
512
  return TaskResponse(task_id=request.task_id, status="error", error=str(e))
513
+ finally:
514
+ state.current_task = None
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
 
517
  if __name__ == "__main__":
requirements.txt CHANGED
@@ -1,14 +1,14 @@
1
  # ALTYZEN Ghost Runner - Requirements
2
- # Standard Python environment - no suspicious keywords
3
 
4
  fastapi>=0.104.0
5
- uvicorn>=0.24.0
6
  pydantic>=2.0.0
7
  python-dotenv>=1.0.0
8
  httpx>=0.25.0
9
  Pillow>=10.0.0
10
 
11
- # Browser automation (installs at runtime)
12
  browser-use>=0.1.0
13
  playwright>=1.40.0
14
  langchain-openai>=0.0.5
 
1
  # ALTYZEN Ghost Runner - Requirements
2
+ # Standard Python environment
3
 
4
  fastapi>=0.104.0
5
+ uvicorn[standard]>=0.24.0
6
  pydantic>=2.0.0
7
  python-dotenv>=1.0.0
8
  httpx>=0.25.0
9
  Pillow>=10.0.0
10
 
11
+ # Browser automation
12
  browser-use>=0.1.0
13
  playwright>=1.40.0
14
  langchain-openai>=0.0.5