"""Same-origin gateway: hosted defaults or isolated BYOK; separate from local Bearer.""" import asyncio import hmac import ipaddress import time from pathlib import Path import aiohttp from aiohttp import web from . import commands, server from .model_catalog import ModelCatalog from .protocol import DecisionError, fields from .web_config import COOKIE, configure, public_config, website_origin from .web_sessions import Sessions, Visitor VISITOR = web.RequestKey("website_visitor", Visitor) CLIENT_IP = web.RequestKey("website_ip", str) MANAGER = web.AppKey("website_sessions", Sessions) CATALOG = web.AppKey("website_catalog", ModelCatalog) PREFIX = server.PREFIX def view(visitor): s = visitor.service return { "version": "lekiwi-agent-v1", "configVersion": s.epoch, "configuration": public_config(s.connections.values), "ready": bool(s.connections.values), "active": len(s.active), "keyStorage": "server-managed" if visitor.hosted else "memory-only", "configurationManaged": visitor.hosted, } def create_website_app( origin, directory=None, *, development=False, limits=None, trusted_proxies=(), defaults=None, budget=None, ): host = website_origin(origin, development=development) if defaults and budget is None: raise ValueError("hosted_mode_requires_persistent_budget") manager = Sessions( directory or Path("/tmp/cadworld-sessions"), origin, limits, defaults=defaults ) catalog = ModelCatalog() cookie = "cadworld-dev-session" if development else COOKIE trusted = set(trusted_proxies) def client_ip(request): remote = request.remote or "unknown" if remote in trusted: try: return str(ipaddress.ip_address(request.headers.get("X-Real-IP", ""))) except ValueError: raise DecisionError("invalid_proxy_ip", 403) from None return remote @web.middleware async def boundary(request, handler): visitor = None try: if request.headers.get("Host") != host: raise DecisionError("host_forbidden", 403) supplied_origin = request.headers.get("Origin") if supplied_origin and supplied_origin != origin: raise DecisionError("origin_forbidden", 403) if request.headers.get("Sec-Fetch-Site") in ("cross-site", "same-site"): raise DecisionError("origin_forbidden", 403) if request.query_string: raise DecisionError("query_forbidden", 400) if request.path == "/healthz" and request.method == "GET": response = web.json_response({"ok": True}) else: await manager.expire() write = request.method not in ("GET", "HEAD") if write and supplied_origin != origin: raise DecisionError("origin_required", 403) bootstrap = request.path == PREFIX + "/session" and request.method == "POST" visitor = manager.values.get(request.cookies.get(cookie, "")) if bootstrap: fields(await server.body(request), []) # Restoring a cookie is read-only: don't extend credential lifetime. if visitor is None: visitor = manager.create(client_ip(request)) response = web.json_response({**view(visitor), "csrfToken": visitor.csrf}) response.set_cookie( cookie, visitor.ident, secure=not development, httponly=True, samesite="Strict", path="/", ) else: if visitor is None or visitor.closed: raise DecisionError("session_expired", 401) if write: if not hmac.compare_digest( request.headers.get("X-CSRF-Token", ""), visitor.csrf ): raise DecisionError("csrf_required", 403) if request.path not in (PREFIX + "/session", PREFIX + "/cancel") and ( request.headers.get("X-Config-Version") != str(visitor.service.epoch) ): raise DecisionError("configuration_changed", 409) visitor.touched = time.monotonic() if manager.defaults and ( request.path.startswith(PREFIX + "/codex/") or request.path in (PREFIX + "/configuration", PREFIX + "/models", PREFIX + "/test") ): raise DecisionError("hosted_configuration_locked", 403) request[VISITOR] = visitor request[server.WEB_SERVICE] = visitor.service request[CLIENT_IP] = client_ip(request) response = await handler(request) except DecisionError as exc: response = web.json_response({"error": exc.code}, status=exc.status) except web.HTTPException as exc: response = web.json_response({"error": "http_request_rejected"}, status=exc.status) except Exception: response = web.json_response({"error": "internal_error"}, status=500) response.headers.update({"Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"}) if visitor and not visitor.closed: response.headers["X-Config-Version"] = str(visitor.service.epoch) return response app = web.Application(middlewares=[boundary], client_max_size=65536) app[MANAGER], app[CATALOG] = manager, catalog async def health(_): return web.json_response({"ok": True}) async def session(_): # Bootstrap handled in middleware, deliberately independent of model/CLI availability. raise DecisionError("invalid_session_method", 405) async def destroy(request): await manager.destroy(request[VISITOR]) response = web.json_response({"cleared": True}) response.del_cookie( cookie, path="/", secure=not development, httponly=True, samesite="Strict" ) return response async def status(request): return web.json_response(view(request[VISITOR])) async def models(_): await catalog.refresh(manager.http) return web.json_response(catalog.public()) async def configuration(request): visitor = request[VISITOR] s = visitor.service epoch = s.epoch data = await server.body(request) fields(data, ["llm", "jev"]) fields(data["llm"], ["provider", "model"], ["apiKey"]) codex_models = () if data["llm"]["provider"] == "openrouter": await catalog.refresh(manager.http) elif data["llm"]["provider"] == "codex": if not visitor.codex_reserved or not (await s.codex.status())["loggedIn"]: raise DecisionError("codex_chatgpt_login_required", 409) codex_models = [m["id"] for m in (await s.codex.models())["models"]] if visitor.closed or s.epoch != epoch: raise DecisionError("configuration_changed", 409) values = configure(s.connections.values, data, catalog.models, codex_models) s.invalidate() s.connections.values = values return web.json_response(view(visitor)) async def inference(request): visitor = request[VISITOR] operation = request.match_info["operation"] llm = visitor.service.connections.values.get("llm") is_llm = operation in ("plan", "command") or ( operation == "test" and (await server.body(request)).get("role") == "llm" ) if is_llm and llm and llm.protocol == "codex" and not visitor.codex_reserved: raise DecisionError("codex_chatgpt_login_required", 409) manager.rate(request[CLIENT_IP], "calls", manager.limits.ip_calls) if manager.inference >= manager.limits.inference: raise DecisionError("server_busy", 429) if budget is not None: budget.reserve(2 if operation == "command" else 1) manager.inference += 1 try: if operation == "command": return web.json_response( await commands.command(visitor.service, await server.body(request)) ) return await { "plan": server.plan, "decide": server.decide, "test": server.test_connection, }[request.match_info["operation"]](request) finally: manager.inference -= 1 async def codex(request): visitor = request[VISITOR] s = visitor.service operation = request.match_info["operation"] if visitor.account_lock.locked(): raise DecisionError("subscription_busy", 409) async with visitor.account_lock: if visitor.closed: raise DecisionError("session_expired", 401) if request.method == "POST": fields(await server.body(request), []) s.invalidate() if operation == "login": manager.rate(request[CLIENT_IP], "logins", manager.limits.ip_logins) manager.reserve_codex(visitor) try: value = await s.codex.login(device=True) visitor.login_deadline = time.monotonic() + 600 except BaseException: await manager.close_codex(visitor) raise elif operation in ("cancel", "logout"): await manager.close_codex(visitor) value = {"loggedIn": False} elif not visitor.codex_reserved: value = {"loggedIn": False, "models": [], "planningAvailable": False} else: value = await { "status": s.codex.status, "models": s.codex.models, "limits": s.codex.limits, }[operation]() if operation == "status" and value.get("loggedIn"): visitor.login_deadline = 0 return web.json_response(value) app.router.add_get("/healthz", health) app.router.add_post(PREFIX + "/session", session) app.router.add_delete(PREFIX + "/session", destroy) app.router.add_get(PREFIX + "/status", status) app.router.add_get(PREFIX + "/models", models) app.router.add_put(PREFIX + "/configuration", configuration) app.router.add_post(PREFIX + "/{operation:plan|decide|test|command}", inference) app.router.add_post(PREFIX + "/cancel", server.cancel) app.router.add_get(PREFIX + "/codex/{operation:status|models|limits}", codex) app.router.add_post(PREFIX + "/codex/{operation:login|cancel|logout}", codex) async def reap(): while True: await asyncio.sleep(15) await manager.expire() async def lifecycle(_): async with aiohttp.ClientSession( trust_env=False, connector=aiohttp.TCPConnector(limit=16) ) as http: manager.http = http reaper = asyncio.create_task(reap()) yield reaper.cancel() await asyncio.gather(reaper, return_exceptions=True) await manager.close() app.cleanup_ctx.append(lifecycle) return app