From 4bc0fb89010c61c6c27234afd0c1dc04fde12d36 Mon Sep 17 00:00:00 2001 From: menzelj Date: Mon, 8 Jun 2026 11:21:28 +0000 Subject: [PATCH] Fix remote-stack log streaming: URL-encode agent token in WS proxy (0.13.2) The agent-logs WebSocket proxy injected the agent token raw into the upstream query string (?token=). Tokens containing base64/url-special characters (+, /, =) were then mangled by the query parser on the agent side (e.g. "+" decoded to a space), so the agent rejected the stream with close code 4401 even though the same token works for the HTTP API (where it travels in the Authorization header). URL-encode the token with urllib.parse.quote so it round-trips intact. Co-Authored-By: Claude Opus 4.8 --- backend/agent_app.py | 2 +- backend/main.py | 2 +- backend/routers/ws.py | 5 ++++- frontend/package.json | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/agent_app.py b/backend/agent_app.py index 14f99d6..7070f79 100644 --- a/backend/agent_app.py +++ b/backend/agent_app.py @@ -40,7 +40,7 @@ from services import backup_service, compose_service logger = logging.getLogger("stackpilot.agent") -AGENT_VERSION = "0.13.1" +AGENT_VERSION = "0.13.2" # --------------------------------------------------------------------------- # diff --git a/backend/main.py b/backend/main.py index 11b87af..05b7d71 100644 --- a/backend/main.py +++ b/backend/main.py @@ -55,7 +55,7 @@ async def lifespan(app: FastAPI): schedule_task.cancel() -app = FastAPI(title="StackPilot", version="0.13.1", lifespan=lifespan) +app = FastAPI(title="StackPilot", version="0.13.2", lifespan=lifespan) app.add_middleware( CORSMiddleware, diff --git a/backend/routers/ws.py b/backend/routers/ws.py index f1c9aef..c0ca7e0 100644 --- a/backend/routers/ws.py +++ b/backend/routers/ws.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import json import logging +import urllib.parse import contextlib @@ -114,7 +115,9 @@ async def ws_agent_logs( ws_url = ("wss://" + base[8:] if base.startswith("https://") else "ws://" + base[7:] if base.startswith("http://") else "ws://" + base) - ws_url += f"/agent/ws/logs/{stack_id}?token={agent.token}" + # URL-encode the token: agent tokens may contain base64 chars (+ / =) that + # would otherwise be mangled in the query string and rejected as 4401. + ws_url += f"/agent/ws/logs/{stack_id}?token={urllib.parse.quote(agent.token, safe='')}" async def _err(detail: str) -> None: with contextlib.suppress(Exception): diff --git a/frontend/package.json b/frontend/package.json index eee0039..c0bf660 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "stackpilot-frontend", "private": true, - "version": "0.13.1", + "version": "0.13.2", "type": "module", "scripts": { "dev": "vite",