"""Production coordinator for same-gateway hosted Discussion rooms.""" from __future__ import annotations import contextlib import hashlib import os import threading import time from collections import Counter from collections.abc import Iterator, Mapping from dataclasses import replace from pathlib import Path from types import ModuleType from typing import Any from gateway import hosted_room_discussion as discussion from gateway import hosted_room_driver as driver from gateway import hosted_room_links from gateway import hosted_rooms from gateway.hosted_room_policy_checkpoint import ( HostedRoomPolicyCheckpoint, PolicySnapshot, ) from gateway.hosted_room_peer import ( GatewayRoomCatalog, HostedMemberDispatch, PROTOCOL_VERSION, ) from tui_gateway.hosted_room_driver import HostedRoomBinding, HostedRoomRuntime from tui_gateway.hosted_room_server_rpc import HostedRoomServerRPC from tui_gateway.hosted_room_peer_http import PeerRunsHTTPClient, PeerRunsHTTPError from tui_gateway.hosted_room_peer_transport import ( HostedRoomPeerClient, PeerHostedRoomTransport, PeerMemberRoute, ) _HOSTED_ROOM_IDLE_FALLBACK_SECONDS = 5.0 _HOSTED_ROOM_ACTIVE_POLL_SECONDS = 0.25 _HOSTED_ROOM_TERMINAL_GRACE_SECONDS = 30.0 def _hosted_room_turn_timeout_seconds() -> float: try: agent_timeout = float(os.getenv("HERMES_AGENT_TIMEOUT", "1800")) except (TypeError, ValueError): agent_timeout = 1800.0 if agent_timeout <= 0: agent_timeout = 1800.0 return agent_timeout + _HOSTED_ROOM_TERMINAL_GRACE_SECONDS def _grant_revoke_is_terminal(exc: PeerRunsHTTPError) -> bool: """Return whether the peer proves the scoped grant is already unusable.""" return exc.status_code in {401, 403} and exc.error_code in { "invalid_room_grant", "room_reauthorization_required", } class HostedRoomService: """Own the hosted Discussion policy and its transport-free worker.""" def __init__( self, server: ModuleType, *, db_path: Path | str | None = None, peer_routes: Mapping[tuple[str, str], PeerMemberRoute] | None = None, peer_clients: Mapping[Any, HostedRoomPeerClient] | None = None, ) -> None: self.server = server self.db_path = Path(db_path or hosted_rooms.default_db_path()) hosted_rooms.prune_disbanded_rooms(self.db_path) self._policy_lock = threading.RLock() self._pending_actions: dict[tuple[str, str], dict[str, Any]] = {} self.policy_checkpoint = HostedRoomPolicyCheckpoint(self.db_path) self.rpc = HostedRoomServerRPC(server) self._link_load_error = None self._peer_route_status: dict[tuple[str, str], str] = {} self.peer_routes = {} self.peer_clients = {} try: stored_links, load_errors = hosted_room_links.load_room_links_tolerant( self.db_path ) errors = list(load_errors) for stored in stored_links: if PROTOCOL_VERSION not in stored.catalog.protocol_versions: errors.append( f"{stored.room_id}:{stored.member_id}:protocol-upgrade-required" ) continue client = PeerRunsHTTPClient( base_url=stored.target_url, api_key="", receipt_db_path=self.db_path, ) route = PeerMemberRoute( home_install_id=hosted_rooms.local_authority_gateway_id(), member_id=stored.member_id, target_install_id=stored.catalog.installation_id, target_profile=stored.target_profile, capability_digest=stored.catalog.catalog_digest, execution_policy_digest=( stored.catalog.execution_policy.policy_digest ), cancellation_scope_id=stored.cancellation_scope_id, trace_id=stored.trace_id, grant=stored.grant, ) self.peer_routes[(stored.room_id, stored.member_id)] = route self.peer_clients[(stored.room_id, stored.member_id)] = client self._peer_route_status[(stored.room_id, stored.member_id)] = ( stored.status ) if errors: self._link_load_error = ",".join(errors) except Exception as exc: self._link_load_error = str(exc) supplied_routes = dict(peer_routes or {}) supplied_clients = dict(peer_clients or {}) self.peer_routes.update(supplied_routes) for key, route in supplied_routes.items(): client = supplied_clients.get(key) if client is None: client = supplied_clients.get(route.target_install_id) if client is not None: self.peer_clients[key] = client self.runtime = HostedRoomRuntime( db_path=self.db_path, rooms=self.bindings, rpc=self.rpc, transport_resolver=self._resolve_member_transport, turn_lock=self._turn_lock, prepare_room=self.prepare_room, publish_terminal=self.publish_terminal, pending_action=self._set_pending_action, poll_interval_seconds=_HOSTED_ROOM_IDLE_FALLBACK_SECONDS, active_poll_interval_seconds=_HOSTED_ROOM_ACTIVE_POLL_SECONDS, turn_timeout_seconds=_hosted_room_turn_timeout_seconds(), ) @property def root(self) -> Path: return self.db_path.parent def local_profiles(self) -> tuple[str, ...]: profiles = {"default"} profiles_dir = self.root / "profiles" if profiles_dir.is_dir(): profiles.update( path.name for path in profiles_dir.iterdir() if path.is_dir() ) return tuple(sorted(profiles)) def bindings(self) -> tuple[HostedRoomBinding, ...]: local_gateway_id = hosted_rooms.local_authority_gateway_id() return tuple( HostedRoomBinding( room_id=str(room["room_id"]), gateway_id=str(room["authority_gateway_id"]), authority_epoch=int(room["authority_epoch"]), ) for room in hosted_rooms.list_rooms(self.db_path) if str(room["authority_gateway_id"]) == local_gateway_id ) def _owned_room(self, room_id: str) -> dict[str, Any]: room = hosted_rooms.room_state(self.db_path, room_id=room_id) if str(room["authority_gateway_id"]) != ( hosted_rooms.local_authority_gateway_id() ): raise hosted_rooms.AuthorityConflictError( "This Group Chat is managed by another gateway." ) return room @contextlib.contextmanager def _turn_lock(self, profile: str) -> Iterator[None]: from tools.bot_relay import acquire_turn_lock with acquire_turn_lock(self.root, profile): yield def start(self) -> None: self.runtime.start() def stop(self, *, timeout: float = 5.0) -> bool: return self.runtime.stop(timeout=timeout) def wakeup(self) -> None: self.runtime.wakeup() def register_peer_route( self, *, room_id: str, member_id: str, route: PeerMemberRoute, client: HostedRoomPeerClient, target_url: str | None = None, catalog: GatewayRoomCatalog | None = None, ) -> None: """Register one verified route and optionally persist its scoped grant.""" bind_store = getattr(client, "bind_receipt_store", None) if callable(bind_store): bind_store(self.db_path) if catalog is not None: if not route.execution_policy_digest: route = replace( route, execution_policy_digest=( catalog.execution_policy.policy_digest ), ) if ( route.capability_digest != catalog.catalog_digest or route.execution_policy_digest != catalog.execution_policy.policy_digest ): raise ValueError("peer route does not match its target catalog") if target_url is not None and catalog is not None: hosted_room_links.save_room_link( self.db_path, hosted_room_links.make_stored_link( room_id=room_id, member_id=member_id, target_url=target_url, target_profile=route.target_profile, grant=route.grant, catalog=catalog, cancellation_scope_id=route.cancellation_scope_id, trace_id=route.trace_id, ), ) # Persistence is the publication boundary. A failed disk write must # never leave a process-local route that disappears after restart. with self._policy_lock: self.peer_routes[(room_id, member_id)] = route self.peer_clients[(room_id, member_id)] = client self._peer_route_status[(room_id, member_id)] = "ready" self.runtime.wakeup() def revoke_room_routes(self, room_id: str) -> int: """Revoke and forget every scoped peer route for one room. The remote revocation is the boundary: if a target is unreachable the room remains intact and the user may retry rather than receiving a false successful disband while a grant is still live. """ with self._policy_lock: routes = [ (key, route) for key, route in self.peer_routes.items() if key[0] == room_id ] for key, route in routes: client = self.peer_clients.get(key) revoke = getattr(client, "revoke_grant", None) if not callable(revoke): raise RuntimeError("peer room grant cannot be revoked safely") try: revoke(grant=route.grant) except PeerRunsHTTPError as exc: if not _grant_revoke_is_terminal(exc): raise hosted_rooms.delete_room_link_records(self.db_path, room_id=room_id) with self._policy_lock: for key, route in routes: self.peer_routes.pop(key, None) self._peer_route_status.pop(key, None) self.peer_clients.pop(key, None) return len(routes) def _resolve_member_transport( self, binding: HostedRoomBinding, task: Mapping[str, Any], ): payload = task.get("payload", {}) member_id = str( payload.get("target_member_id") or payload.get("target_profile") or "" ) route = self.peer_routes.get((binding.room_id, member_id)) if route is None: if self._member_is_peer(binding.room_id, member_id): raise RuntimeError("peer room route is unavailable") return self.rpc client = self.peer_clients.get((binding.room_id, member_id)) if client is None: raise RuntimeError("peer room client is unavailable") identity = task.get("identity") execution_generation = int(task.get("execution_generation") or 0) bind_observation = getattr(client, "bind_observation", None) if ( callable(bind_observation) and isinstance(identity, driver.TaskIdentity) and execution_generation > 0 ): bind_observation( task_id=identity.task_id, execution_generation=execution_generation, ) tracked_client = _RouteStatusPeerClient( client, on_ready=lambda: self._set_route_status( binding.room_id, member_id, "ready" ), on_reauthorization=lambda: self._set_route_status( binding.room_id, member_id, "needs_reauthorization" ), on_unavailable=lambda: self._set_route_status( binding.room_id, member_id, "unavailable" ), on_refreshed=lambda grant, catalog=None: self._rotate_route_grant( binding.room_id, member_id, grant, catalog ), ) self._recover_peer_admission(binding, task, route, tracked_client) return PeerHostedRoomTransport( binding=binding, route=route, client=tracked_client, source_event_seq=int(payload.get("source_event_seq") or 0), task_id=getattr(task.get("identity"), "task_id", None), execution_generation=int(task.get("execution_generation") or 0), ) def _recover_peer_admission( self, binding: HostedRoomBinding, task: Mapping[str, Any], route: PeerMemberRoute, client: Any, ) -> None: """Rediscover an admitted peer run without advancing its generation.""" recover = getattr(client, "recover_dispatch", None) identity = task.get("identity") payload = task.get("payload") execution_generation = int(task.get("execution_generation") or 0) if ( not callable(recover) or not isinstance(identity, driver.TaskIdentity) or not isinstance(payload, Mapping) or execution_generation < 1 or task.get("status") not in {"running", "indeterminate", "stopping"} ): return prompt = payload.get("prompt") source_event_seq = int(payload.get("source_event_seq") or 0) if not isinstance(prompt, str) or source_event_seq < 1 or not route.trace_id: raise RuntimeError("peer room admission identity is unavailable for recovery") dispatch = HostedMemberDispatch.from_mapping({ "protocol_version": PROTOCOL_VERSION, "room_id": identity.room_id, "home_install_id": route.home_install_id, "authority_gateway_id": binding.gateway_id, "authority_epoch": binding.authority_epoch, "member_id": route.member_id, "target_install_id": route.target_install_id, "target_profile": route.target_profile, "task_id": identity.task_id, "execution_generation": execution_generation, "source_event_seq": source_event_seq, "cancellation_scope_id": route.cancellation_scope_id, "prompt": prompt, "prompt_digest": hashlib.sha256(prompt.encode("utf-8")).hexdigest(), "capability_digest": route.capability_digest, "execution_policy_digest": route.execution_policy_digest, "trace_id": route.trace_id, }) recover(dispatch=dispatch.as_mapping(), grant=route.grant) def _member_is_peer(self, room_id: str, member_id: str) -> bool: room = hosted_rooms.room_state(self.db_path, room_id=room_id) for member in room.get("members") or []: if not isinstance(member, Mapping): continue if str(member.get("member_id") or member.get("profile") or "") != member_id: continue target = member.get("target") return isinstance(target, Mapping) and target.get("kind") == "peer" return False def _set_route_status(self, room_id: str, member_id: str, status: str) -> None: key = (room_id, member_id) with self._policy_lock: if self._peer_route_status.get(key) == status: return self._peer_route_status[key] = status hosted_room_links.mark_room_link_status( self.db_path, room_id=room_id, member_id=member_id, status=status, ) def _set_pending_action( self, room_id: str, member_id: str, action: Mapping[str, Any] | None, ) -> None: key = (room_id, member_id) with self._policy_lock: if action is None: self._pending_actions.pop(key, None) else: self._pending_actions[key] = {**action, "member_id": member_id} def _rotate_route_grant( self, room_id: str, member_id: str, grant: str, catalog: GatewayRoomCatalog | None = None, ) -> None: """Persist a target-refreshed scoped grant before publishing it live.""" key = (room_id, member_id) route = self.peer_routes.get(key) if route is None: raise RuntimeError("peer room route is unavailable") stored = next( ( link for link in hosted_room_links.load_room_links(self.db_path) if (link.room_id, link.member_id) == key ), None, ) if stored is None: raise RuntimeError("peer room route cannot be renewed before persistence") effective_catalog = catalog or stored.catalog if catalog is not None and ( catalog.installation_id != route.target_install_id or catalog.execution_policy.target_profile != route.target_profile or PROTOCOL_VERSION not in catalog.protocol_versions or "direct" not in catalog.link_modes or not catalog.text or catalog.execution_policy.policy_digest != route.execution_policy_digest ): self._set_route_status(room_id, member_id, "needs_reauthorization") raise RuntimeError( "peer room execution policy changed; reauthorization is required" ) rotated_route = replace( route, grant=grant, capability_digest=( catalog.catalog_digest if catalog is not None else route.capability_digest ), execution_policy_digest=( catalog.execution_policy.policy_digest if catalog is not None else route.execution_policy_digest ), ) hosted_room_links.save_room_link( self.db_path, hosted_room_links.make_stored_link( room_id=room_id, member_id=member_id, target_url=stored.target_url, target_profile=stored.target_profile, grant=grant, catalog=effective_catalog, cancellation_scope_id=stored.cancellation_scope_id, trace_id=stored.trace_id, ), ) with self._policy_lock: self.peer_routes[key] = rotated_route self._peer_route_status[key] = "ready" def _route_statuses(self, room_id: str | None = None) -> list[dict[str, str]]: with self._policy_lock: rows = [ { "room_id": key[0], "member_id": key[1], "status": status, } for key, status in self._peer_route_status.items() if room_id is None or key[0] == room_id ] return sorted(rows, key=lambda row: (row["room_id"], row["member_id"])) def _events(self, room_id: str) -> list[dict[str, Any]]: events: list[dict[str, Any]] = [] cursor = 0 while True: page = hosted_rooms.read_events( self.db_path, room_id=room_id, since_seq=cursor, limit=hosted_rooms.MAX_LOG_LIMIT, ) rows = page.get("events") if isinstance(rows, list): events.extend(row for row in rows if isinstance(row, dict)) next_cursor = int(page.get("cursor") or cursor) if not page.get("has_more"): return events if next_cursor <= cursor: raise RuntimeError("hosted room replay cursor did not advance") cursor = next_cursor def _append_plan(self, room_id: str, plan: discussion.PublicationPlan) -> None: for event in plan.events: hosted_rooms.append_event( self.db_path, **event.append_kwargs(room_id), ) def _policy_snapshot(self, room: Mapping[str, Any]) -> PolicySnapshot: return self.policy_checkpoint.snapshot( room_id=str(room["room_id"]), latest_seq=int(room["latest_seq"]), ) def _publish_terminal_tasks( self, room: Mapping[str, Any], ) -> bool: changed = False local_profiles = self.local_profiles() for status in ("deferred", "settled", "failed", "cancelled"): for task in driver.list_tasks( self.db_path, room_id=str(room["room_id"]), status=status, ): identity = task["identity"] if self.policy_checkpoint.publication_exists( room_id=str(room["room_id"]), task_id=identity.task_id, status=status, execution_generation=int(task["execution_generation"]), ): continue task_events = self.policy_checkpoint.events_for_task( room_id=str(room["room_id"]), source_event_seq=int(task["payload"]["source_event_seq"]), ) plan = discussion.reconstruct_task_plan( room, task_events, task, local_profiles=local_profiles, ) publication = discussion.plan_publication( room, task_events, plan, status=status, result=task.get("result"), execution_generation=( int(task["execution_generation"]) if status == "deferred" else None ), local_profiles=local_profiles, ) self._append_plan(str(room["room_id"]), publication) changed = True return changed def _append_room_status( self, room: Mapping[str, Any], decision: discussion.DiscussionDecision, ) -> None: if decision.discussion_event_id is None: return hosted_rooms.append_event( self.db_path, room_id=str(room["room_id"]), event_id=f"dactivity:{decision.discussion_event_id}:{decision.reason}", kind="room.activity", actor={"kind": "gateway", "id": str(room["authority_gateway_id"])}, payload={ "status": decision.status, "reason_code": decision.reason, "thread_id": decision.thread_id, "discussion_event_id": decision.discussion_event_id, }, authority_gateway_id=str(room["authority_gateway_id"]), authority_epoch=int(room["authority_epoch"]), ) def prepare_room(self, binding: HostedRoomBinding) -> None: with self._policy_lock: room = hosted_rooms.room_state(self.db_path, room_id=binding.room_id) snapshot = self._policy_snapshot(room) events = list(snapshot.events) if self._publish_terminal_tasks(room): room = hosted_rooms.room_state( self.db_path, room_id=binding.room_id, ) snapshot = self._policy_snapshot(room) events = list(snapshot.events) self.policy_checkpoint.compact_completed(room_id=binding.room_id) driver.prune_published_terminal_tasks( self.db_path, room_id=binding.room_id, clock=self.runtime.clock, ) if any( driver.list_tasks( self.db_path, room_id=binding.room_id, status=status, ) for status in ("queued", "running", "stopping") ): return decision = discussion.plan_next_task( room, events, local_profiles=self.local_profiles(), initial_watermarks=snapshot.watermarks, ) if decision.status == "task" and decision.task is not None: driver.admit_task( self.db_path, decision.task.identity, payload=decision.task.payload, clock=time.time, ) # A stop can race the policy read from another process. Re-read # after admission and cancel before the runtime can execute a # task whose source event is now behind the room stop fence. fresh_room = hosted_rooms.room_state( self.db_path, room_id=binding.room_id, ) stopped_through_seq = self._policy_snapshot( fresh_room ).stopped_through_seq if ( decision.source_event_seq is not None and decision.source_event_seq < stopped_through_seq ): self.runtime.cancel( decision.task.identity, cancel_id=f"stop-fence:{stopped_through_seq}", ) elif decision.status in {"settled", "bounded"}: self._append_room_status(room, decision) def publish_terminal( self, binding: HostedRoomBinding, _task: Mapping[str, Any], ) -> None: self.prepare_room(binding) self.runtime.wakeup() def create_room(self, *, room_id: str, name: str, members: Any) -> dict[str, Any]: normalized = discussion.validate_roster( members, local_profiles=self.local_profiles(), ) room = hosted_rooms.create_room( self.db_path, room_id=room_id, name=name, members=[ { "member_id": member.member_id, "profile": member.profile, "handle": member.handle, "target": dict(member.target or {}), **( {"display_name": member.display_name} if member.display_name else {} ), } for member in normalized ], authority_gateway_id=hosted_rooms.local_authority_gateway_id(), ) self.runtime.wakeup() return room def send( self, *, room_id: str, event_id: str, payload: Any, ) -> dict[str, Any]: normalized = discussion.validate_user_payload(payload) room = self._owned_room(room_id) event = hosted_rooms.append_event( self.db_path, room_id=room_id, event_id=event_id, kind="message.user", actor={"kind": "user", "id": "desktop"}, payload=normalized, authority_gateway_id=str(room["authority_gateway_id"]), authority_epoch=int(room["authority_epoch"]), ) binding = next( ( candidate for candidate in self.bindings() if candidate.room_id == room_id ), None, ) if binding is None: raise hosted_rooms.RoomNotFoundError("hosted room not found") self.prepare_room(binding) self.runtime.wakeup() return event def stop_room( self, room_id: str, *, cancel_id: str, require_acknowledged: bool = False, ) -> int: room = self._owned_room(room_id) hosted_rooms.request_room_stop( self.db_path, room_id=room_id, cancel_id=cancel_id, expected_gateway_id=str(room["authority_gateway_id"]), expected_epoch=int(room["authority_epoch"]), ) cancelled = 0 pending = 0 with self._policy_lock: tasks = {} for status in ( "queued", "running", "indeterminate", "deferred", "stopping", ): for task in driver.list_tasks( self.db_path, room_id=room_id, status=status, ): identity = task["identity"] tasks[(identity.room_id, identity.task_id)] = task for task in tasks.values(): task_cancel_id = ( str(task.get("cancel_id") or "") if task.get("status") == "stopping" else "" ) result = self.runtime.cancel( task["identity"], cancel_id=task_cancel_id or cancel_id, ) cancelled += 1 if result["status"] == "stopping": pending += 1 if require_acknowledged and pending: raise RuntimeError( "room work is still stopping; retry deletion after Stop completes" ) self.runtime.wakeup() return cancelled def retry_room_task(self, room_id: str, *, task_id: str) -> dict[str, Any]: """Retry one uncertain or deferred task only after explicit user action.""" task = next( ( candidate for status in ("indeterminate", "deferred") for candidate in driver.list_tasks( self.db_path, room_id=room_id, status=status ) if candidate["identity"].task_id == task_id ), None, ) if task is None: raise driver.InvalidTaskTransitionError( "no retryable room task matches task_id" ) return self.runtime.retry_indeterminate(task["identity"]) def approve_room_task( self, room_id: str, *, member_id: str, task_id: str, execution_generation: int, choice: str, request_id: str | None = None, ) -> Mapping[str, Any]: """Resolve one exact local or peer approval and wake room observation.""" key = (room_id, member_id) route = self.peer_routes.get(key) client = self.peer_clients.get(key) with self._policy_lock: action = self._pending_actions.get(key) requested_approval_id = str(request_id or "") pending_approval_id = str((action or {}).get("request_id") or "") if ( action is None or action.get("task_id") != task_id or int(action.get("execution_generation") or 0) != execution_generation or not requested_approval_id or not pending_approval_id or requested_approval_id != pending_approval_id ): raise RuntimeError("room approval is no longer pending") if choice not in {"once", "deny"}: raise RuntimeError("room approval choice must be once or deny") approve = getattr(client, "approve_receipt", None) if route is not None and callable(approve): result = approve( task_id=task_id, execution_generation=execution_generation, request_id=requested_approval_id, choice=choice, grant=route.grant, ) else: session_id = str(action.get("session_id") or "") if not session_id: raise RuntimeError("local room approval identity is unavailable") result = self.rpc.approve( session_id=session_id, request_id=requested_approval_id, choice=choice, ) if result is None: raise RuntimeError("room approval target is unavailable") with self._policy_lock: current = self._pending_actions.get(key) if ( current is not None and str(current.get("request_id") or "") == requested_approval_id and current.get("task_id") == task_id and int(current.get("execution_generation") or 0) == execution_generation ): self._pending_actions.pop(key, None) self.runtime.wakeup() return result def status(self, room_id: str | None = None) -> dict[str, Any]: runtime = self.runtime.status() runtime = {**runtime, "peer_routes": self._route_statuses(room_id)} if self._link_load_error: runtime = {**runtime, "link_load_error": self._link_load_error} if room_id is None: return runtime tasks = driver.list_tasks(self.db_path, room_id=room_id) counts = Counter(str(task["status"]) for task in tasks) pending_actions = [ { "kind": "retry", "task_id": task["identity"].task_id, } for task in tasks if task["status"] in {"indeterminate", "deferred"} ] with self._policy_lock: pending_actions.extend( dict(action) for ( action_room_id, _member_id, ), action in self._pending_actions.items() if action_room_id == room_id ) return { "running": runtime["running"], "working": bool( counts.get("running") or counts.get("queued") or counts.get("stopping") ), "blocked": room_id in runtime["blocked_rooms"] or bool(counts.get("indeterminate") or counts.get("stopping")), "counts": dict(counts), "pending_actions": pending_actions, "peer_routes": self._route_statuses(room_id), } class _RouteStatusPeerClient: """Classify scoped-auth failures without exposing route credentials.""" def __init__( self, client, *, on_ready, on_reauthorization, on_unavailable, on_refreshed, ) -> None: self._client = client self._on_ready = on_ready self._on_reauthorization = on_reauthorization self._on_unavailable = on_unavailable self._on_refreshed = on_refreshed def __getattr__(self, name): value = getattr(self._client, name) if not callable(value): return value def tracked(*args, **kwargs): if name in {"dispatch", "recover_dispatch"} and "grant" in kwargs: from gateway.hosted_room_peer import ( room_grant_needs_dispatch_refresh, ) grant = kwargs["grant"] if room_grant_needs_dispatch_refresh(grant): checked = HostedMemberDispatch.from_mapping( kwargs["dispatch"] ) refresh = getattr(self._client, "refresh_grant", None) if callable(refresh): try: refreshed = refresh( grant=grant, capability_digest=checked.capability_digest, execution_policy_digest=( checked.execution_policy_digest ), ) except Exception as exc: if bool( getattr(exc, "needs_reauthorization", False) ): self._on_reauthorization() raise if room_grant_needs_dispatch_refresh( grant, leeway_seconds=0 ): self._on_reauthorization() raise else: replacement = str(refreshed.get("grant") or "") if not replacement: raise RuntimeError( "peer returned no refreshed room grant" ) refreshed_catalog = None if refreshed.get("catalog") is not None: from gateway.hosted_room_peer import ( GatewayRoomCatalog, ) refreshed_catalog = GatewayRoomCatalog.from_mapping( refreshed.get("catalog") ) if ( refreshed_catalog.execution_policy.policy_digest != checked.execution_policy_digest ): self._on_reauthorization() raise PeerRunsHTTPError( "peer room execution policy needs reauthorization", status_code=403, error_code="room_execution_policy_changed", not_admitted=True, ) if ( refreshed_catalog.catalog_digest != checked.capability_digest ): self._on_reauthorization() raise PeerRunsHTTPError( "peer room capabilities need reauthorization", status_code=403, error_code="room_capability_catalog_changed", not_admitted=True, ) self._on_refreshed(replacement, refreshed_catalog) kwargs = {**kwargs, "grant": replacement} try: result = value(*args, **kwargs) except Exception as exc: if bool(getattr(exc, "needs_reauthorization", False)): self._on_reauthorization() raise elif bool(getattr(exc, "not_admitted", False)): self._on_unavailable() raise else: raise if name != "prepare": self._on_ready() return result return tracked