Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,866 @@
|
||||
"""Hosted-room JSON-RPC contract.
|
||||
|
||||
These methods expose durable room identity, replay, and the process-owned
|
||||
same-gateway Discussion driver. ``groups.capabilities`` keeps that boundary
|
||||
machine-readable so older clients stay on the renderer-owned room path.
|
||||
"""
|
||||
|
||||
from .method_ctx import HandlerRegistry
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
_registry = HandlerRegistry()
|
||||
method = _registry.method
|
||||
|
||||
LONG_HANDLERS = frozenset({
|
||||
"groups.list",
|
||||
"groups.capabilities",
|
||||
"groups.create",
|
||||
"groups.state",
|
||||
"groups.send",
|
||||
"groups.rename",
|
||||
"groups.log",
|
||||
"groups.disband",
|
||||
"groups.replicate",
|
||||
"groups.replica_state",
|
||||
"groups.promote",
|
||||
"groups.demote",
|
||||
"groups.stop",
|
||||
"groups.retry",
|
||||
"groups.approve",
|
||||
"groups.peer.invite",
|
||||
"groups.peer.revoke",
|
||||
"groups.peer.register",
|
||||
})
|
||||
|
||||
_service_lock = threading.Lock()
|
||||
_run_store_lock = threading.Lock()
|
||||
_bound_server = None
|
||||
_service = None
|
||||
|
||||
|
||||
def bind_server(server) -> None:
|
||||
"""Bind the fully initialized server module without starting a worker."""
|
||||
|
||||
global _bound_server
|
||||
_bound_server = server
|
||||
server._profile_execution_policy = _profile_execution_policy
|
||||
|
||||
|
||||
def start_hosted_room_service():
|
||||
"""Start one process-owned hosted room service idempotently."""
|
||||
|
||||
global _service
|
||||
if _bound_server is None:
|
||||
return None
|
||||
from gateway.hosted_rooms import default_db_path
|
||||
from tui_gateway.hosted_room_service import HostedRoomService
|
||||
|
||||
db_path = default_db_path()
|
||||
with _service_lock:
|
||||
if _service is not None and _service.db_path != db_path:
|
||||
_service.stop(timeout=1.0)
|
||||
_service = None
|
||||
if _service is None:
|
||||
_service = HostedRoomService(_bound_server, db_path=db_path)
|
||||
_service.start()
|
||||
return _service
|
||||
|
||||
|
||||
def stop_hosted_room_service(*, timeout: float = 5.0) -> bool:
|
||||
"""Stop the process-owned worker without interrupting accepted turns."""
|
||||
|
||||
global _service
|
||||
with _service_lock:
|
||||
service = _service
|
||||
if service is None:
|
||||
return True
|
||||
stopped = service.stop(timeout=timeout)
|
||||
if stopped and _service is service:
|
||||
_service = None
|
||||
return stopped
|
||||
|
||||
|
||||
def get_hosted_room_service():
|
||||
"""Return the active service, if its lifecycle owner started it."""
|
||||
|
||||
service = _service
|
||||
if service is None:
|
||||
return None
|
||||
try:
|
||||
status = service.runtime.status()
|
||||
except Exception:
|
||||
return None
|
||||
return service if status.get("running") and not status.get("stopping") else None
|
||||
|
||||
|
||||
_WORKER_UNAVAILABLE = (
|
||||
"Group Chat worker is unavailable. Restart the Hermes gateway and try again."
|
||||
)
|
||||
|
||||
|
||||
def _profile_name() -> str:
|
||||
return (os.getenv("HERMES_PROFILE") or "default").strip() or "default"
|
||||
|
||||
|
||||
def _requested_profile(params: dict) -> str:
|
||||
requested = str(params.get("profile") or "").strip()
|
||||
if not requested:
|
||||
return _profile_name()
|
||||
if _bound_server is None:
|
||||
raise ValueError("profile routing is unavailable")
|
||||
current = str(_bound_server._current_profile_name() or "").strip()
|
||||
if requested == current:
|
||||
return current
|
||||
home = _bound_server._profile_home(requested)
|
||||
if home is None:
|
||||
raise ValueError(f"profile '{requested}' is unavailable")
|
||||
return str(_bound_server._response_profile_name(requested) or requested)
|
||||
|
||||
|
||||
def _api_server_key(profile: str | None = None) -> str:
|
||||
if profile and _bound_server is not None:
|
||||
current = str(_bound_server._current_profile_name() or "").strip()
|
||||
if profile != current:
|
||||
from agent.secret_scope import build_profile_secret_scope
|
||||
|
||||
home = _bound_server._profile_home(profile)
|
||||
if home is None:
|
||||
return ""
|
||||
# An explicit routed profile is authoritative. Never borrow the
|
||||
# process/default profile's API key on a multiplexed gateway.
|
||||
return str(
|
||||
build_profile_secret_scope(home).get("API_SERVER_KEY") or ""
|
||||
).strip()
|
||||
try:
|
||||
from agent.secret_scope import get_secret
|
||||
|
||||
scoped = (get_secret("API_SERVER_KEY", "") or "").strip()
|
||||
if scoped:
|
||||
return scoped
|
||||
except Exception:
|
||||
pass
|
||||
return (os.getenv("API_SERVER_KEY") or "").strip()
|
||||
|
||||
|
||||
def _profile_execution_policy(profile: str) -> dict:
|
||||
"""Resolve execution policy under the exact multiplexed profile home."""
|
||||
|
||||
from gateway.hosted_room_execution_policy import execution_policy_mapping
|
||||
from hermes_constants import (
|
||||
reset_hermes_home_override,
|
||||
set_hermes_home_override,
|
||||
)
|
||||
|
||||
token = None
|
||||
if _bound_server is not None:
|
||||
current = str(_bound_server._current_profile_name() or "").strip()
|
||||
if profile not in {current, _profile_name()}:
|
||||
home = _bound_server._profile_home(profile)
|
||||
if home is None:
|
||||
raise ValueError(f"profile '{profile}' is unavailable")
|
||||
token = set_hermes_home_override(str(home))
|
||||
try:
|
||||
return execution_policy_mapping(target_profile=profile)
|
||||
finally:
|
||||
if token is not None:
|
||||
reset_hermes_home_override(token)
|
||||
|
||||
|
||||
def _room_link_run_storage_durable() -> bool:
|
||||
"""Return whether peer-run replay survives this gateway process."""
|
||||
|
||||
if _bound_server is None:
|
||||
# Direct method-contract tests and embedded callers without a bound API
|
||||
# server do not expose peer-run transport. The production server always
|
||||
# binds before advertising capabilities.
|
||||
return True
|
||||
store = getattr(_bound_server, "_run_idempotency_store", None)
|
||||
if store is None:
|
||||
# The dashboard/TUI process owns groups.* but does not construct the
|
||||
# API adapter that normally owns this store. Open the same shared
|
||||
# SQLite-backed store lazily so capability negotiation reflects the
|
||||
# real /v1/runs replay boundary instead of depending on test-only
|
||||
# injection. A separately enabled API adapter uses the same file.
|
||||
from gateway.platforms.api_server import RunIdempotencyStore
|
||||
|
||||
with _run_store_lock:
|
||||
store = getattr(_bound_server, "_run_idempotency_store", None)
|
||||
if store is None:
|
||||
store = RunIdempotencyStore()
|
||||
_bound_server._run_idempotency_store = store
|
||||
return bool(getattr(store, "durable", False))
|
||||
|
||||
|
||||
@method("groups.capabilities")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Describe the hosted-room protocol implemented by this gateway."""
|
||||
from gateway.hosted_rooms import (
|
||||
MAX_LOG_LIMIT,
|
||||
PROTOCOL_VERSION,
|
||||
local_authority_gateway_id,
|
||||
)
|
||||
|
||||
service = get_hosted_room_service()
|
||||
driver_ready = bool(service and service.runtime.status()["running"])
|
||||
try:
|
||||
from gateway.hosted_room_peer import (
|
||||
PROTOCOL_VERSION as ROOM_LINK_PROTOCOL_VERSION,
|
||||
gateway_room_grant_secret,
|
||||
local_catalog_mapping,
|
||||
)
|
||||
|
||||
profile = _requested_profile(params)
|
||||
if not _room_link_run_storage_durable():
|
||||
raise ValueError("durable run idempotency storage is required")
|
||||
gateway_room_grant_secret()
|
||||
catalog = local_catalog_mapping(
|
||||
installation_id=local_authority_gateway_id(),
|
||||
protocol_versions=(ROOM_LINK_PROTOCOL_VERSION,),
|
||||
link_modes=("direct",),
|
||||
text=True,
|
||||
attachments=False,
|
||||
target_profile=profile,
|
||||
execution_policy=_profile_execution_policy(profile),
|
||||
)
|
||||
room_link = {
|
||||
"enabled": True,
|
||||
"profile": profile,
|
||||
"catalog": catalog,
|
||||
"endpoint": catalog["endpoint"],
|
||||
}
|
||||
except Exception:
|
||||
room_link = {
|
||||
"enabled": False,
|
||||
"reason": (
|
||||
"durable_run_storage_required"
|
||||
if not _room_link_run_storage_durable()
|
||||
else "gateway_roomlink_secret_unavailable"
|
||||
),
|
||||
}
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"protocol_version": PROTOCOL_VERSION,
|
||||
"driver": driver_ready,
|
||||
"persistent_process": bool(
|
||||
room_link.get("catalog", {}).get("persistent_process", False)
|
||||
),
|
||||
"authority_gateway_id": local_authority_gateway_id(),
|
||||
"room_link": room_link,
|
||||
"features": [
|
||||
"authority_epoch",
|
||||
"coordinator_fencing",
|
||||
"room_identity",
|
||||
"monotonic_log",
|
||||
"idempotent_send",
|
||||
"replayable_disband",
|
||||
"typed_events",
|
||||
"actor_identity",
|
||||
"log_replication",
|
||||
"authority_takeover",
|
||||
],
|
||||
"methods": [
|
||||
"groups.capabilities",
|
||||
"groups.list",
|
||||
"groups.create",
|
||||
"groups.state",
|
||||
"groups.send",
|
||||
"groups.rename",
|
||||
"groups.log",
|
||||
"groups.disband",
|
||||
"groups.replicate",
|
||||
"groups.replica_state",
|
||||
"groups.promote",
|
||||
"groups.demote",
|
||||
"groups.stop",
|
||||
"groups.retry",
|
||||
"groups.approve",
|
||||
"groups.peer.invite",
|
||||
"groups.peer.revoke",
|
||||
"groups.peer.register",
|
||||
],
|
||||
"max_log_limit": MAX_LOG_LIMIT,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@method("groups.peer.invite")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Mint one target-issued room/profile grant for a prospective home."""
|
||||
try:
|
||||
from gateway.hosted_room_peer import (
|
||||
PROTOCOL_VERSION as ROOM_LINK_PROTOCOL_VERSION,
|
||||
decode_room_grant,
|
||||
gateway_room_grant_secret,
|
||||
issue_room_grant,
|
||||
local_catalog_mapping,
|
||||
)
|
||||
from gateway import hosted_rooms
|
||||
|
||||
if not _room_link_run_storage_durable():
|
||||
raise ValueError("durable run idempotency storage is required")
|
||||
installation_id = hosted_rooms.local_authority_gateway_id()
|
||||
profile = _requested_profile(params)
|
||||
ttl = float(params.get("ttl_seconds", 3600))
|
||||
if not 60 <= ttl <= 24 * 60 * 60:
|
||||
raise ValueError("ttl_seconds must be between 60 and 86400")
|
||||
grant_secret = gateway_room_grant_secret()
|
||||
execution_policy = _profile_execution_policy(profile)
|
||||
token = issue_room_grant(
|
||||
grant_secret,
|
||||
grant_id=str(params.get("grant_id") or f"grant-{os.urandom(16).hex()}"),
|
||||
room_id=str(params.get("room_id") or ""),
|
||||
home_install_id=str(params.get("home_install_id") or ""),
|
||||
authority_gateway_id=str(
|
||||
params.get("authority_gateway_id") or ""
|
||||
),
|
||||
authority_epoch=int(params.get("authority_epoch") or 0),
|
||||
member_id=str(params.get("member_id") or ""),
|
||||
target_install_id=installation_id,
|
||||
target_profile=profile,
|
||||
execution_policy_digest=execution_policy["policy_digest"],
|
||||
ttl_seconds=ttl,
|
||||
)
|
||||
claims = decode_room_grant(grant_secret, token, permission="status")
|
||||
hosted_rooms.reserve_peer_room(
|
||||
hosted_rooms.default_db_path(),
|
||||
claims=claims,
|
||||
expires_at=float(claims.get("status_expires_at", claims["expires_at"])),
|
||||
)
|
||||
catalog = local_catalog_mapping(
|
||||
installation_id=installation_id,
|
||||
protocol_versions=(ROOM_LINK_PROTOCOL_VERSION,),
|
||||
link_modes=("direct",),
|
||||
text=True,
|
||||
attachments=False,
|
||||
target_profile=profile,
|
||||
execution_policy=execution_policy,
|
||||
)
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"grant": token,
|
||||
"target_profile": profile,
|
||||
"catalog": catalog,
|
||||
"endpoint": catalog["endpoint"],
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return _err(rid, 4120, str(exc))
|
||||
|
||||
|
||||
@method("groups.peer.revoke")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Revoke one target-issued grant using its exact profile scope."""
|
||||
try:
|
||||
from gateway import hosted_rooms
|
||||
from gateway.hosted_room_peer import decode_room_grant, gateway_room_grant_secret
|
||||
|
||||
profile = _requested_profile(params)
|
||||
claims = decode_room_grant(
|
||||
gateway_room_grant_secret(),
|
||||
str(params.get("grant") or ""),
|
||||
permission="status",
|
||||
)
|
||||
if (
|
||||
claims["target_profile"] != profile
|
||||
or claims["target_install_id"]
|
||||
!= hosted_rooms.local_authority_gateway_id()
|
||||
):
|
||||
raise ValueError("room grant target does not match this profile")
|
||||
hosted_rooms.revoke_room_grant_scope(
|
||||
hosted_rooms.default_db_path(),
|
||||
claims=claims,
|
||||
expires_at=float(
|
||||
claims.get("status_expires_at", claims["expires_at"])
|
||||
),
|
||||
)
|
||||
return _ok(rid, {"revoked": True})
|
||||
except Exception as exc:
|
||||
return _err(rid, 4122, str(exc))
|
||||
|
||||
|
||||
@method("groups.peer.register")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Register and probe one scoped target route on the room home."""
|
||||
service = get_hosted_room_service()
|
||||
if service is None:
|
||||
return _err(rid, 4121, "hosted room driver is unavailable")
|
||||
try:
|
||||
from gateway.hosted_room_peer import (
|
||||
GatewayRoomCatalog,
|
||||
PROTOCOL_VERSION as ROOM_LINK_PROTOCOL_VERSION,
|
||||
validate_room_link_url,
|
||||
)
|
||||
from gateway.hosted_rooms import local_authority_gateway_id, room_state
|
||||
from tui_gateway.hosted_room_peer_http import PeerRunsHTTPClient
|
||||
from tui_gateway.hosted_room_peer_transport import PeerMemberRoute
|
||||
|
||||
target_url, transport_security = validate_room_link_url(
|
||||
params.get("target_url")
|
||||
)
|
||||
catalog = GatewayRoomCatalog.from_mapping(params.get("catalog"))
|
||||
if ROOM_LINK_PROTOCOL_VERSION not in catalog.protocol_versions:
|
||||
raise ValueError(
|
||||
f"target does not support RoomLink protocol v{ROOM_LINK_PROTOCOL_VERSION}"
|
||||
)
|
||||
if "direct" not in catalog.link_modes:
|
||||
raise ValueError("target does not support a direct RoomLink")
|
||||
target_profile = str(params.get("target_profile") or "")
|
||||
grant = str(params.get("grant") or "")
|
||||
client = PeerRunsHTTPClient(
|
||||
base_url=target_url,
|
||||
api_key="",
|
||||
receipt_db_path=service.db_path,
|
||||
)
|
||||
probe = client.probe(grant=grant)
|
||||
live_catalog = GatewayRoomCatalog.from_mapping(probe.get("catalog"))
|
||||
if live_catalog != catalog:
|
||||
raise ValueError("target capability catalog changed during setup")
|
||||
if (
|
||||
ROOM_LINK_PROTOCOL_VERSION not in live_catalog.protocol_versions
|
||||
or "direct" not in live_catalog.link_modes
|
||||
):
|
||||
raise ValueError("target RoomLink capability is incompatible")
|
||||
room_id = str(params.get("room_id") or "")
|
||||
member_id = str(params.get("member_id") or "")
|
||||
home_install_id = local_authority_gateway_id()
|
||||
home_room = room_state(service.db_path, room_id=room_id)
|
||||
if (
|
||||
probe.get("room_id") != room_id
|
||||
or probe.get("home_install_id") != home_install_id
|
||||
or probe.get("authority_gateway_id")
|
||||
!= home_room.get("authority_gateway_id")
|
||||
or int(probe.get("authority_epoch") or 0)
|
||||
!= int(home_room.get("authority_epoch") or 0)
|
||||
or probe.get("member_id") != member_id
|
||||
or probe.get("target_profile") != target_profile
|
||||
):
|
||||
raise ValueError("room grant scope does not match this route")
|
||||
route = PeerMemberRoute(
|
||||
home_install_id=home_install_id,
|
||||
member_id=member_id,
|
||||
target_install_id=catalog.installation_id,
|
||||
target_profile=target_profile,
|
||||
capability_digest=catalog.catalog_digest,
|
||||
execution_policy_digest=catalog.execution_policy.policy_digest,
|
||||
cancellation_scope_id=str(
|
||||
params.get("cancellation_scope_id")
|
||||
or f"cancel-{params.get('room_id') or ''}"
|
||||
),
|
||||
trace_id=str(params.get("trace_id") or f"trace-{os.urandom(16).hex()}"),
|
||||
grant=grant,
|
||||
)
|
||||
service.register_peer_route(
|
||||
room_id=room_id,
|
||||
member_id=member_id,
|
||||
route=route,
|
||||
client=client,
|
||||
target_url=target_url,
|
||||
catalog=catalog,
|
||||
)
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"registered": True,
|
||||
"mode": "direct",
|
||||
"transport_security": transport_security,
|
||||
"target_install_id": catalog.installation_id,
|
||||
"target_profile": target_profile,
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return _err(rid, 5120, str(exc))
|
||||
|
||||
|
||||
@method("groups.list")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""List rooms hosted by this gateway."""
|
||||
try:
|
||||
from gateway.hosted_rooms import (
|
||||
MAX_ROOM_LIST_LIMIT,
|
||||
default_db_path,
|
||||
list_rooms,
|
||||
)
|
||||
|
||||
limit = params.get("limit", MAX_ROOM_LIST_LIMIT)
|
||||
offset = params.get("offset", 0)
|
||||
rooms = list_rooms(
|
||||
default_db_path(),
|
||||
include_disbanded=params.get("include_disbanded") is True,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"rooms": rooms,
|
||||
"next_offset": offset + limit if len(rooms) == limit else None,
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return _err(rid, 5110, str(exc))
|
||||
|
||||
|
||||
@method("groups.create")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Create a hosted room idempotently.
|
||||
|
||||
Required params: ``room_id``, ``name``, and ``members``. Authority is
|
||||
derived from this gateway's stable install identity, never from the client.
|
||||
"""
|
||||
from gateway.hosted_rooms import HostedRoomError
|
||||
|
||||
try:
|
||||
service = get_hosted_room_service()
|
||||
if service is None:
|
||||
return _err(rid, 4123, _WORKER_UNAVAILABLE)
|
||||
room = service.create_room(
|
||||
room_id=params.get("room_id"),
|
||||
name=params.get("name"),
|
||||
members=params.get("members"),
|
||||
)
|
||||
return _ok(rid, {"room": room})
|
||||
except HostedRoomError as exc:
|
||||
reason = getattr(exc, "reason", None)
|
||||
return _err(rid, 4110, str(exc), {"reason": reason} if reason else None)
|
||||
except Exception as exc:
|
||||
return _err(rid, 5111, str(exc))
|
||||
|
||||
|
||||
@method("groups.state")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Return one hosted room's replay cursor and fenced authority state."""
|
||||
from gateway.hosted_rooms import HostedRoomError, default_db_path, room_state
|
||||
|
||||
try:
|
||||
room = room_state(
|
||||
default_db_path(),
|
||||
room_id=params.get("room_id"),
|
||||
include_disbanded=params.get("include_disbanded") is True,
|
||||
)
|
||||
service = get_hosted_room_service()
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"room": room,
|
||||
**(
|
||||
{"driver_status": service.status(str(room["room_id"]))}
|
||||
if service is not None and room.get("disbanded_at") is None
|
||||
else {}
|
||||
),
|
||||
},
|
||||
)
|
||||
except HostedRoomError as exc:
|
||||
reason = getattr(exc, "reason", None)
|
||||
return _err(rid, 4114, str(exc), {"reason": reason} if reason else None)
|
||||
except Exception as exc:
|
||||
return _err(rid, 5115, str(exc))
|
||||
|
||||
|
||||
@method("groups.send")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Append one typed event to a hosted room idempotently.
|
||||
|
||||
Required params: ``room_id``, ``event_id``, and object ``payload``. Only
|
||||
inert ``message.user`` events are accepted through this client-facing
|
||||
method. The actor is server-owned rather than trusted from params.
|
||||
Admission is durable; no Bot turn is started by this slice.
|
||||
"""
|
||||
from gateway.hosted_rooms import HostedRoomError, user_event_id
|
||||
|
||||
try:
|
||||
client_event_id = params.get("event_id")
|
||||
service = get_hosted_room_service()
|
||||
if service is None:
|
||||
return _err(rid, 4123, _WORKER_UNAVAILABLE)
|
||||
event = service.send(
|
||||
room_id=params.get("room_id"),
|
||||
event_id=user_event_id(client_event_id),
|
||||
payload=params.get("payload"),
|
||||
)
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"event": event,
|
||||
"client_event_id": client_event_id,
|
||||
"accepted": True,
|
||||
"driver_started": True,
|
||||
},
|
||||
)
|
||||
except HostedRoomError as exc:
|
||||
reason = getattr(exc, "reason", None)
|
||||
return _err(rid, 4111, str(exc), {"reason": reason} if reason else None)
|
||||
except Exception as exc:
|
||||
return _err(rid, 5112, str(exc))
|
||||
|
||||
|
||||
@method("groups.rename")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Rename one hosted room atomically with its replay event."""
|
||||
from gateway.hosted_rooms import HostedRoomError, default_db_path, rename_room
|
||||
|
||||
try:
|
||||
renamed = rename_room(
|
||||
default_db_path(),
|
||||
room_id=params.get("room_id"),
|
||||
event_id=params.get("event_id"),
|
||||
name=params.get("name"),
|
||||
)
|
||||
return _ok(rid, {"room": renamed})
|
||||
except HostedRoomError as exc:
|
||||
reason = getattr(exc, "reason", None)
|
||||
return _err(rid, 4117, str(exc), {"reason": reason} if reason else None)
|
||||
except Exception as exc:
|
||||
return _err(rid, 5117, str(exc))
|
||||
|
||||
|
||||
@method("groups.disband")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Permanently tombstone a hosted room id."""
|
||||
from gateway.hosted_rooms import (
|
||||
AuthorityConflictError,
|
||||
HostedRoomError,
|
||||
RoomHistoryExpiredError,
|
||||
disband_room,
|
||||
local_authority_gateway_id,
|
||||
room_state,
|
||||
)
|
||||
|
||||
try:
|
||||
service = get_hosted_room_service()
|
||||
if service is None:
|
||||
return _err(rid, 4123, _WORKER_UNAVAILABLE)
|
||||
|
||||
def disband_with_state(state: dict | None = None) -> dict:
|
||||
local_gateway_id = local_authority_gateway_id()
|
||||
if state is not None and (
|
||||
str(state["authority_gateway_id"]) != local_gateway_id
|
||||
):
|
||||
raise AuthorityConflictError(
|
||||
"This Group Chat is managed by another gateway."
|
||||
)
|
||||
return disband_room(
|
||||
service.db_path,
|
||||
room_id=params.get("room_id"),
|
||||
expected_gateway_id=str(
|
||||
local_gateway_id
|
||||
),
|
||||
expected_epoch=int(
|
||||
state["authority_epoch"] if state is not None else 1
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
existing = room_state(
|
||||
service.db_path,
|
||||
room_id=params.get("room_id"),
|
||||
include_disbanded=True,
|
||||
)
|
||||
except RoomHistoryExpiredError:
|
||||
tombstone = disband_with_state()
|
||||
return _ok(rid, {"tombstone": tombstone})
|
||||
if existing.get("disbanded_at") is not None:
|
||||
tombstone = disband_with_state(existing)
|
||||
return _ok(rid, {"tombstone": tombstone})
|
||||
service.stop_room(
|
||||
str(params.get("room_id") or ""),
|
||||
cancel_id=str(params.get("cancel_id") or "room-disbanded"),
|
||||
require_acknowledged=True,
|
||||
)
|
||||
service.revoke_room_routes(str(params.get("room_id") or ""))
|
||||
tombstone = disband_with_state(existing)
|
||||
return _ok(rid, {"tombstone": tombstone})
|
||||
except HostedRoomError as exc:
|
||||
reason = getattr(exc, "reason", None)
|
||||
return _err(rid, 4113, str(exc), {"reason": reason} if reason else None)
|
||||
except Exception as exc:
|
||||
return _err(rid, 5114, str(exc))
|
||||
|
||||
|
||||
@method("groups.stop")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Durably cancel queued or running work for one hosted room."""
|
||||
|
||||
service = get_hosted_room_service()
|
||||
if service is None:
|
||||
return _err(rid, 4115, "hosted room driver is unavailable")
|
||||
try:
|
||||
count = service.stop_room(
|
||||
str(params.get("room_id") or ""),
|
||||
cancel_id=str(params.get("cancel_id") or "desktop-stop"),
|
||||
)
|
||||
return _ok(rid, {"cancelled": count})
|
||||
except Exception as exc:
|
||||
return _err(rid, 5116, str(exc))
|
||||
|
||||
|
||||
@method("groups.approve")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Resolve one exact approval requested by a local or peer room member."""
|
||||
|
||||
service = get_hosted_room_service()
|
||||
if service is None:
|
||||
return _err(rid, 4115, "hosted room driver is unavailable")
|
||||
try:
|
||||
result = service.approve_room_task(
|
||||
str(params.get("room_id") or ""),
|
||||
member_id=str(params.get("member_id") or ""),
|
||||
task_id=str(params.get("task_id") or ""),
|
||||
execution_generation=int(params.get("execution_generation") or 0),
|
||||
choice=str(params.get("choice") or ""),
|
||||
request_id=str(params.get("request_id") or ""),
|
||||
)
|
||||
return _ok(rid, {"approved": True, "result": result})
|
||||
except Exception as exc:
|
||||
return _err(rid, 5119, str(exc))
|
||||
|
||||
|
||||
@method("groups.retry")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Retry one indeterminate room task after explicit user confirmation."""
|
||||
service = get_hosted_room_service()
|
||||
if service is None:
|
||||
return _err(rid, 4115, "hosted room driver is unavailable")
|
||||
try:
|
||||
task = service.retry_room_task(
|
||||
str(params.get("room_id") or ""),
|
||||
task_id=str(params.get("task_id") or ""),
|
||||
)
|
||||
identity = task.get("identity") if isinstance(task, dict) else None
|
||||
receipt = {
|
||||
"room_id": str(getattr(identity, "room_id", "") or ""),
|
||||
"task_id": str(getattr(identity, "task_id", "") or ""),
|
||||
"thread_id": str(getattr(identity, "thread_id", "") or ""),
|
||||
"turn_id": str(getattr(identity, "turn_id", "") or ""),
|
||||
"status": str(task.get("status") or "") if isinstance(task, dict) else "",
|
||||
"execution_generation": int(task.get("execution_generation") or 0)
|
||||
if isinstance(task, dict)
|
||||
else 0,
|
||||
"cancel_generation": int(task.get("cancel_generation") or 0)
|
||||
if isinstance(task, dict)
|
||||
else 0,
|
||||
}
|
||||
return _ok(rid, {"retried": True, "task": receipt})
|
||||
except Exception as exc:
|
||||
return _err(rid, 5118, str(exc))
|
||||
|
||||
|
||||
@method("groups.log")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Return a monotonic room-log delta after ``since_seq``."""
|
||||
from gateway.hosted_rooms import HostedRoomError, default_db_path, read_events
|
||||
|
||||
try:
|
||||
delta = read_events(
|
||||
default_db_path(),
|
||||
room_id=params.get("room_id"),
|
||||
since_seq=params.get("since_seq", 0),
|
||||
limit=params.get("limit", 100),
|
||||
include_disbanded=params.get("include_disbanded") is True,
|
||||
)
|
||||
return _ok(rid, delta)
|
||||
except HostedRoomError as exc:
|
||||
reason = getattr(exc, "reason", None)
|
||||
return _err(rid, 4112, str(exc), {"reason": reason} if reason else None)
|
||||
except Exception as exc:
|
||||
return _err(rid, 5113, str(exc))
|
||||
|
||||
|
||||
@method("groups.replicate")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Persist one authority-stamped replay page into the local replica store.
|
||||
|
||||
``page`` is the verbatim ``groups.log`` result read from the room's
|
||||
authority gateway; ingest is idempotent and refuses sequence gaps and
|
||||
authority-epoch regressions.
|
||||
"""
|
||||
from gateway.hosted_room_replicas import ReplicaError, ingest_page
|
||||
from gateway.hosted_rooms import default_db_path
|
||||
|
||||
try:
|
||||
result = ingest_page(
|
||||
default_db_path(),
|
||||
room_id=params.get("room_id"),
|
||||
room_name=params.get("room_name"),
|
||||
members=params.get("members"),
|
||||
page=params.get("page"),
|
||||
)
|
||||
return _ok(rid, result)
|
||||
except ReplicaError as exc:
|
||||
return _err(rid, 4116, str(exc))
|
||||
except Exception as exc:
|
||||
return _err(rid, 5116, str(exc))
|
||||
|
||||
|
||||
@method("groups.replica_state")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Report the local replica's coverage and authority lineage."""
|
||||
from gateway.hosted_room_replicas import ReplicaError, replica_state
|
||||
from gateway.hosted_rooms import default_db_path
|
||||
|
||||
try:
|
||||
return _ok(rid, replica_state(default_db_path(), room_id=params.get("room_id")))
|
||||
except ReplicaError as exc:
|
||||
return _err(rid, 4117, str(exc))
|
||||
except Exception as exc:
|
||||
return _err(rid, 5117, str(exc))
|
||||
|
||||
|
||||
@method("groups.promote")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Continue a replicated room on THIS gateway at ``epoch + 1``.
|
||||
|
||||
Requires ``confirm: true`` — the caller asserts the previous authority can
|
||||
no longer commit (explicit user action; a lease/quorum driver later).
|
||||
"""
|
||||
from gateway.hosted_room_replicas import ReplicaError, promote_replica
|
||||
from gateway.hosted_rooms import HostedRoomError, default_db_path
|
||||
|
||||
if params.get("confirm") is not True:
|
||||
return _err(
|
||||
rid,
|
||||
4118,
|
||||
"promotion requires confirm=true acknowledging the previous "
|
||||
"authority can no longer commit",
|
||||
)
|
||||
try:
|
||||
result = promote_replica(
|
||||
default_db_path(),
|
||||
room_id=params.get("room_id"),
|
||||
reason=params.get("reason", "authority-unreachable"),
|
||||
)
|
||||
return _ok(rid, result)
|
||||
except ReplicaError as exc:
|
||||
return _err(rid, 4118, str(exc))
|
||||
except HostedRoomError as exc:
|
||||
return _err(rid, 4118, str(exc))
|
||||
except Exception as exc:
|
||||
return _err(rid, 5118, str(exc))
|
||||
|
||||
|
||||
@method("groups.demote")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Fence this gateway's stale room authority against a proven newer epoch."""
|
||||
from gateway.hosted_room_replicas import ReplicaError, demote_room
|
||||
from gateway.hosted_rooms import default_db_path
|
||||
|
||||
try:
|
||||
result = demote_room(
|
||||
default_db_path(),
|
||||
room_id=params.get("room_id"),
|
||||
observed_gateway_id=params.get("observed_gateway_id"),
|
||||
observed_epoch=params.get("observed_epoch"),
|
||||
)
|
||||
return _ok(rid, result)
|
||||
except ReplicaError as exc:
|
||||
return _err(rid, 4119, str(exc))
|
||||
except Exception as exc:
|
||||
return _err(rid, 5119, str(exc))
|
||||
|
||||
|
||||
def register(server) -> None:
|
||||
_registry.install(server)
|
||||
Reference in New Issue
Block a user