[F029] websocket: remove broken /api/permissions/check loopback from channel stream

channel_stream called validate_channel_access, which HTTP-loopbacked to
GET /api/permissions/check — a route that does not exist. Every call 404'd
-> False -> the channel stream closed with WS_1008_POLICY_VIOLATION for
EVERY client, so the real-time channel stream was dead. Removed the
function, its call site, and the now-unused httpx + settings imports.

Post-F004 the panel-token gate is the channel-stream authorization (the
CEO panel is the sole WS client and may view every channel), so the
broken loopback is removed rather than replaced with an in-process check
the CEO always passes. The legitimate enforcement.validate_channel_access
(slugs, in-process static ACL) is a different function and is untouched.

F027 is resolved-by-F004 (no code change): all three per-agent streams
gate on _require_panel_token first, so only the authorized CEO panel can
connect — 'any viewer subscribes to any target' is closed.

TDD; ruff/mypy clean; 530 unit/api+enforcement+RBAC tests green.
This commit is contained in:
Renn F
2026-06-28 17:52:14 +02:00
parent dc047d7e5b
commit c43c1b057f
4 changed files with 52 additions and 41 deletions
+52 -1
View File
@@ -19,7 +19,12 @@ from uuid import uuid4
import pytest
from fastapi import WebSocketDisconnect, status
from roboco.agents_config import CEO_AGENT_ID, issue_agent_token
from roboco.api.websocket import agent_stream, notification_stream
from roboco.api.websocket import (
ConnectionManager,
agent_stream,
channel_stream,
notification_stream,
)
if TYPE_CHECKING:
import pytest as _pytest # noqa: F401
@@ -119,3 +124,49 @@ async def test_agent_stream_rejects_missing_token_when_required(
ws.close.assert_awaited_once()
assert ws.close.await_args.kwargs["code"] == status.WS_1008_POLICY_VIOLATION
ws.accept.assert_not_awaited()
# ---------------------------------------------------------------------------
# The channel stream must be usable by a panel-token holder. It previously
# called validate_channel_access, which HTTP-loopbacked to a non-existent
# /api/permissions/check endpoint — every connection 404'd → False → the stream
# closed with WS_1008_POLICY_VIOLATION for every client (the channel live-stream
# was dead). Post-F004 the panel-token gate IS the channel-stream authorization
# (the CEO panel is the sole WS client and may view every channel), so the
# broken loopback check is removed rather than replaced with theater.
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_channel_stream_accepts_panel_token_holder(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A panel-token holder supplying an agent_id query param is accepted and
registered on the channel stream — not fail-closed by a dead permission
check that 404s against a non-existent endpoint."""
monkeypatch.setenv("ROBOCO_AGENT_AUTH_SECRET", _SECRET)
monkeypatch.setenv("ROBOCO_AGENT_AUTH_REQUIRED", "true")
token = issue_agent_token(CEO_AGENT_ID, "ceo", "")
channel_id = uuid4()
viewer_id = uuid4()
mgr = ConnectionManager()
ws = _mock_ws(
headers={"x-agent-token": token},
query={"agent_id": str(viewer_id)},
)
monkeypatch.setattr("roboco.api.websocket.manager", mgr)
await channel_stream(ws, channel_id)
ws.accept.assert_awaited_once()
# Not fail-closed by a dead permission check.
ws.close.assert_not_awaited()
# The "connected" confirmation is sent immediately after connect_channel
# registers the socket, and its subscriber_count proves the socket was in
# the channel's subscription set at confirmation time (the mock then raises
# WebSocketDisconnect so the finally disconnects it — the normal clean
# exit, not a fail-close).
confirmation = ws.send_json.await_args.args[0]
assert confirmation["type"] == "connected"
assert confirmation["channel_id"] == str(channel_id)
assert confirmation["subscriber_count"] == 1