mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-13 07:40:18 +00:00
port aisuite#380: slack installer pre-add, mcp oauth quarantine, automation toast, e2e fixes
Slack installer joins the workspace allow-list on managed connect; MCP interactive oauth only from explicit connects. Run-started toast over a new app-wide /ws/events socket; Automations e2e locators scoped to the account menu.
This commit is contained in:
@@ -1657,6 +1657,24 @@ def create_app(manager: SessionManager) -> FastAPI:
|
||||
finally:
|
||||
manager.unregister_session_client(session_id, ws.send_json)
|
||||
|
||||
@app.websocket("/ws/events")
|
||||
async def ws_events(ws: WebSocket) -> None:
|
||||
"""App-wide event stream (session-independent): the GUI keeps one open for
|
||||
pushes like automation_run_started (the UX-026 toast). Read-only — inbound
|
||||
frames are ignored; the receive loop just detects disconnect."""
|
||||
if not _origin_allowed(ws.headers.get("origin")):
|
||||
await ws.close(code=1008)
|
||||
return
|
||||
await ws.accept()
|
||||
manager.register_event_client(ws.send_json)
|
||||
try:
|
||||
while True:
|
||||
await ws.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
manager.unregister_event_client(ws.send_json)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
@@ -161,6 +161,9 @@ class SessionManager:
|
||||
# whoever drives the turn (foreground user_message, channel delivery, self-wake, resume).
|
||||
# Delivery itself is socket-independent — this only governs *live visibility*.
|
||||
self._session_clients: dict[str, set[Any]] = {}
|
||||
# App-wide event sockets (/ws/events): session-independent pushes — today the
|
||||
# automation-run-started toast (UX-026); badges could ride it later.
|
||||
self._event_clients: set[Any] = set()
|
||||
# Automation: scheduled tasks store + the tick scheduler (started in the lifespan).
|
||||
# The scheduler also resumes self-wake'd sessions each tick (extra_tick).
|
||||
self.task_store = TaskStore(base / "automation.db")
|
||||
@@ -833,9 +836,19 @@ class SessionManager:
|
||||
]
|
||||
try:
|
||||
conn = await self.mcp.ensure(server)
|
||||
except (
|
||||
Exception
|
||||
): # bad command / unreachable url — skip, don't break the session
|
||||
except Exception as exc:
|
||||
if mcp_oauth.is_auth_required(exc):
|
||||
# Stored tokens no longer refresh (vendor rotated/expired
|
||||
# them) — the non-interactive connect refused to open a
|
||||
# browser. Record it so the MCP page shows WHY the server is
|
||||
# dark; the session just runs without its tools.
|
||||
self._mcp_errors[server.name] = (
|
||||
"sign-in required — reconnect this server from its page"
|
||||
)
|
||||
logger.info(
|
||||
"mcp %s needs re-auth; skipped for this session", server.name
|
||||
)
|
||||
# else: bad command / unreachable url — skip, don't break the session
|
||||
continue
|
||||
callables = build_callables(
|
||||
server,
|
||||
@@ -914,7 +927,8 @@ class SessionManager:
|
||||
self._mcp_authorizing.add(name)
|
||||
self._mcp_errors.pop(name, None)
|
||||
try:
|
||||
conn = await self.mcp.ensure(server)
|
||||
# The ONE place a browser sign-in may start: an explicit connect.
|
||||
conn = await self.mcp.ensure(server, interactive=True)
|
||||
return {"ok": True, "tools": len(conn.tools)}
|
||||
except Exception as exc:
|
||||
self._mcp_errors[name] = str(exc) or exc.__class__.__name__
|
||||
@@ -2139,6 +2153,21 @@ class SessionManager:
|
||||
return {"ok": True}
|
||||
|
||||
# -- per-session live view --------------------------------------------------
|
||||
def register_event_client(self, send_cb: Any) -> None:
|
||||
self._event_clients.add(send_cb)
|
||||
|
||||
def unregister_event_client(self, send_cb: Any) -> None:
|
||||
self._event_clients.discard(send_cb)
|
||||
|
||||
async def broadcast_event(self, message: dict) -> None:
|
||||
"""Fan an app-wide event out to every /ws/events socket. Best-effort: a dead
|
||||
socket is dropped, never fatal to the caller."""
|
||||
for cb in list(self._event_clients):
|
||||
try:
|
||||
await cb(message)
|
||||
except Exception:
|
||||
self.unregister_event_client(cb)
|
||||
|
||||
def register_session_client(self, session_id: str, send_cb: Any) -> None:
|
||||
self._session_clients.setdefault(session_id, set()).add(send_cb)
|
||||
|
||||
@@ -2650,6 +2679,22 @@ class SessionManager:
|
||||
task_id=task.id, trigger=trigger
|
||||
) # __post_init__ sets run.session_id
|
||||
self.task_store.add_run(run) # mark "running"
|
||||
# UX-026: tell every open app window a SCHEDULED run just started (the 5s
|
||||
# top-right toast). Manual runs never come through here — the user is
|
||||
# already watching those live.
|
||||
await self.broadcast_event(
|
||||
{
|
||||
"type": "automation_run_started",
|
||||
"data": {
|
||||
"task_id": task.id,
|
||||
"task_title": task.title,
|
||||
"session_id": run.session_id,
|
||||
"workspace": task.workspace,
|
||||
"agent": task.agent,
|
||||
"trigger": trigger,
|
||||
},
|
||||
}
|
||||
)
|
||||
# Each run is a real, persisted conversation thread: it runs the instructions under its
|
||||
# own session id, then saves the transcript. The user can reopen that session and ask a
|
||||
# follow-up — the scheduled agent is no longer fire-and-forget.
|
||||
|
||||
Reference in New Issue
Block a user