feat(webui): polish agent output and app discovery
This commit is contained in:
@@ -32,6 +32,11 @@ from nanobot.bus.outbound_events import (
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.channels.base import BaseChannel
|
||||
from nanobot.config.schema import Base
|
||||
from nanobot.runtime_context import (
|
||||
RUNTIME_CONTEXT_INPUT_META,
|
||||
WEBUI_QUOTE_METADATA,
|
||||
webui_quote_runtime_context,
|
||||
)
|
||||
from nanobot.security.workspace_access import (
|
||||
WORKSPACE_SCOPE_METADATA_KEY,
|
||||
WorkspaceScopeError,
|
||||
@@ -250,6 +255,8 @@ class WebSocketChannel(BaseChannel):
|
||||
self._conn_chats: dict[Any, set[str]] = {}
|
||||
# connection -> default chat_id for legacy frames that omit routing.
|
||||
self._conn_default: dict[Any, str] = {}
|
||||
# Connections authenticated with a one-time token from /webui/bootstrap.
|
||||
self._webui_connections: set[Any] = set()
|
||||
self._stop_event: asyncio.Event | None = None
|
||||
self._server_task: asyncio.Task[None] | None = None
|
||||
|
||||
@@ -284,6 +291,7 @@ class WebSocketChannel(BaseChannel):
|
||||
if not subs:
|
||||
self._subs.pop(cid, None)
|
||||
self._conn_default.pop(connection, None)
|
||||
self._webui_connections.discard(connection)
|
||||
|
||||
async def _maybe_push_active_goal_state(self, chat_id: str) -> None:
|
||||
"""Replay an active sustained goal from session metadata after *chat_id* is subscribed.
|
||||
@@ -374,19 +382,25 @@ class WebSocketChannel(BaseChannel):
|
||||
if static_token:
|
||||
if supplied and hmac.compare_digest(supplied, static_token):
|
||||
return None
|
||||
if supplied and self._tokens.take_issued_token_if_valid(supplied):
|
||||
if supplied and self._consume_issued_token(connection, supplied):
|
||||
return None
|
||||
return connection.respond(401, "Unauthorized")
|
||||
|
||||
if self.config.websocket_requires_token:
|
||||
if supplied and self._tokens.take_issued_token_if_valid(supplied):
|
||||
if supplied and self._consume_issued_token(connection, supplied):
|
||||
return None
|
||||
return connection.respond(401, "Unauthorized")
|
||||
|
||||
if supplied:
|
||||
self._tokens.take_issued_token_if_valid(supplied)
|
||||
self._consume_issued_token(connection, supplied)
|
||||
return None
|
||||
|
||||
def _consume_issued_token(self, connection: Any, token: str) -> bool:
|
||||
audience = self._tokens.take_issued_token_audience(token)
|
||||
if audience == "webui":
|
||||
self._webui_connections.add(connection)
|
||||
return audience is not None
|
||||
|
||||
# -- Server lifecycle and connection ingress ---------------------------
|
||||
|
||||
async def start(self) -> None:
|
||||
@@ -696,6 +710,12 @@ class WebSocketChannel(BaseChannel):
|
||||
cli_apps=cli_apps or None,
|
||||
mcp_presets=mcp_presets or None,
|
||||
)
|
||||
if metadata.get("webui") is True and connection in self._webui_connections:
|
||||
quote = webui_quote_runtime_context({
|
||||
WEBUI_QUOTE_METADATA: envelope.get("quoted_context"),
|
||||
})
|
||||
if quote is not None:
|
||||
metadata[RUNTIME_CONTEXT_INPUT_META] = [quote]
|
||||
await self._handle_message(
|
||||
sender_id=client_id,
|
||||
chat_id=cid,
|
||||
@@ -747,6 +767,7 @@ class WebSocketChannel(BaseChannel):
|
||||
self._subs.clear()
|
||||
self._conn_chats.clear()
|
||||
self._conn_default.clear()
|
||||
self._webui_connections.clear()
|
||||
self._tokens.clear()
|
||||
|
||||
async def _safe_send_to(self, connection: Any, raw: str, *, label: str = "") -> None:
|
||||
@@ -988,6 +1009,8 @@ class WebSocketChannel(BaseChannel):
|
||||
self._stream_text_buffers.setdefault(stream_key, []).append(delta)
|
||||
if stream_id is not None:
|
||||
body["stream_id"] = stream_id
|
||||
if stream_end and resuming:
|
||||
body["resuming"] = True
|
||||
self._transcripts.prepare_and_append(
|
||||
chat_id,
|
||||
body,
|
||||
|
||||
Reference in New Issue
Block a user