Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/brew/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ def _require_terminal_enabled() -> None:
app.include_router(
terminal_router,
prefix="/api",
dependencies=[Depends(require_api_key), Depends(_require_terminal_enabled)],
# require_api_key is handled inline in the WS handler — FastAPI's WS dep
# resolver doesn't inject Header/Query the same way HTTP does.
dependencies=[Depends(_require_terminal_enabled)],
)

if _mcp_enabled:
Expand Down
19 changes: 18 additions & 1 deletion src/brew/terminal/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,38 @@

from typing import TYPE_CHECKING, Annotated

from fastapi import APIRouter, Depends, WebSocket
from fastapi import APIRouter, Depends, WebSocket, status

from brew.dependencies import get_settings
from brew.terminal.dependencies import get_terminal_service

if TYPE_CHECKING:
from brew.config import Settings
from brew.terminal.service import TerminalService

router = APIRouter()


def _extract_api_key(ws: WebSocket) -> str | None:
return ws.headers.get("x-api-key") or ws.query_params.get("api_key")


@router.websocket("/terminal/ws")
async def terminal_ws(
ws: WebSocket,
service: Annotated[TerminalService, Depends(get_terminal_service)],
settings: Annotated[Settings, Depends(get_settings)],
) -> None:
# Auth inline: include_router(dependencies=[...]) propagates deps to WS
# routes, but FastAPI's WS dependency resolver doesn't inject Header/Query
# parameters the same way HTTP does, so the shared `require_api_key` dep
# can't read the credential. Read directly from the WS scope instead.
if settings.api_key is not None:
provided = _extract_api_key(ws)
if provided is None or provided != settings.api_key.get_secret_value():
await ws.close(code=status.WS_1008_POLICY_VIOLATION)
return

await ws.accept()
async with service.attached() as session:
await session.run(ws)
Loading