Compare commits

...
Author SHA1 Message Date
Hanzo AI 0c269cf5e5 chat: same-origin /v1/agent bridge — the cookie never leaves the host
The SPA called the gateway cross-origin; the session cookie is host-only, so
every signed-in user got 403 on their first message (401 once the cookie was
widened — the gateway takes bearers, not cookies). The bridge forwards the
caller's bearer server-side, the wallet pattern. VITE_AGENT_API still selects
direct mode.
2026-07-17 11:52:09 -07:00
2 changed files with 28 additions and 1 deletions
+23
View File
@@ -889,6 +889,29 @@ def add_studio_home_routes(routes: web.RouteTableDef, server) -> None:
org = _org_of(request)
return web.json_response(await _wallet(request, org))
@routes.post("/v1/agent")
async def agent_bridge(request: web.Request):
"""Same-origin bridge to the cloud agent orchestrator. The session cookie
stays host-only (never widened to *.hanzo.ai); this forwards the caller's
bearer server-side — the wallet pattern. Direct-to-gateway remains
available to the SPA via VITE_AGENT_API."""
from middleware.gpu_dispatch import CLOUD, _bearer
tok = _bearer(request)
if not tok:
return web.json_response({"error": "auth required"}, status=401)
body = await request.read()
if len(body) > 1 << 20:
return web.json_response({"error": "body too large"}, status=413)
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=180)) as s:
async with s.post(f"{CLOUD}/v1/agent", data=body,
headers={"Authorization": tok,
"Content-Type": "application/json"}) as r:
return web.Response(status=r.status, body=await r.read(),
content_type="application/json")
except Exception:
return web.json_response({"error": "agent unreachable"}, status=502)
@routes.get("/v1/library")
async def get_library(request: web.Request):
org = _org_of(request)
+5 -1
View File
@@ -10,7 +10,11 @@
// VITE_STUDIO_API / VITE_AGENT_API override the bases for local dev.
const BASE = (import.meta.env.VITE_STUDIO_API ?? '').replace(/\/$/, '')
const AGENT_API = (import.meta.env.VITE_AGENT_API ?? 'https://api.hanzo.ai').replace(/\/$/, '')
// Default: same-origin through the studio's /v1/agent bridge — the host-only
// session cookie authorizes it and the middleware forwards the bearer, so no
// parent-domain cookie and no gateway CORS dependency. VITE_AGENT_API points
// straight at the gateway for direct mode.
const AGENT_API = (import.meta.env.VITE_AGENT_API ?? '').replace(/\/$/, '')
async function j<T>(url: string, init?: RequestInit): Promise<T> {
const r = await fetch(url, { credentials: 'include', ...init })