643 lines
19 KiB
Python
643 lines
19 KiB
Python
"""Hosted Group Chat rooms (``groups.*``), cross-connection bot relay (``bot_relay.*``) and the
|
|
dashboard browser controller (``browser.controller.*``).
|
|
|
|
Handlers: ``tui_gateway/methods_groups.py``, ``tui_gateway/methods_bot_relay.py``,
|
|
``tui_gateway/methods_browser_control.py``. Room / event / page shapes are produced by
|
|
``gateway/hosted_rooms.py`` (``_room_from_row`` / ``_event_from_row`` / ``read_events``) and
|
|
``gateway/hosted_room_replicas.py``; the RoomLink catalog by ``gateway/hosted_room_peer.py``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .base import JsonValue, Params, Result, WireEnum
|
|
from .common import OkResult, OpenModel, ProfileParams
|
|
from .registry import method
|
|
from .server_requests import ApprovalChoice
|
|
|
|
# ── shared room shapes ────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class RoomMember(OpenModel):
|
|
"""One roster row (``hosted_room_discussion.validate_roster``); legacy rooms may carry
|
|
pre-normalisation rows, so the set stays open."""
|
|
|
|
member_id: str | None = None
|
|
profile: str | None = None
|
|
handle: str | None = None
|
|
display_name: str | None = None
|
|
target: dict[str, JsonValue] | None = None
|
|
|
|
|
|
class RoomActor(Result):
|
|
kind: str
|
|
id: str
|
|
|
|
|
|
class RoomEvent(Result):
|
|
"""``gateway/hosted_rooms.py::_event_from_row``."""
|
|
|
|
room_id: str
|
|
seq: int
|
|
event_id: str
|
|
kind: str
|
|
actor: RoomActor
|
|
authority_epoch: int | None = None
|
|
payload: dict[str, JsonValue]
|
|
created_at: float
|
|
idempotent: bool = False
|
|
|
|
|
|
class Room(Result):
|
|
"""``gateway/hosted_rooms.py::_room_from_row`` plus the branch-only keys ``create`` (legacy
|
|
adoption), ``state`` (``authority_claim``) and ``rename`` (``event``) add."""
|
|
|
|
room_id: str
|
|
name: str
|
|
members: list[RoomMember]
|
|
authority_gateway_id: str
|
|
authority_epoch: int
|
|
revision: int
|
|
created_at: float
|
|
updated_at: float
|
|
idempotent: bool = False
|
|
disbanded_at: float | None = None
|
|
latest_seq: int | None = None
|
|
adopted: bool | None = None
|
|
claim_event: RoomEvent | None = None
|
|
authority_claim: RoomEvent | None = None
|
|
event: RoomEvent | None = None
|
|
|
|
|
|
class RoomAuthority(Result):
|
|
gateway_id: str
|
|
epoch: int
|
|
|
|
|
|
class RoomMemberInput(Params):
|
|
"""A roster row as the client proposes it; ``validate_roster`` owns the exact rules."""
|
|
|
|
member_id: str | None = None
|
|
profile: str | None = None
|
|
handle: str | None = None
|
|
display_name: str | None = None
|
|
target: dict[str, JsonValue] | None = None
|
|
model_config = Params.model_config | {"extra": "allow"}
|
|
|
|
|
|
class RoomParams(ProfileParams):
|
|
"""Any method addressed at one hosted room."""
|
|
|
|
room_id: str
|
|
|
|
|
|
# ── RoomLink catalog ──────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class RoomExecutionPolicy(Result):
|
|
"""``gateway/hosted_room_execution_policy.py::execution_policy_mapping``."""
|
|
|
|
version: int
|
|
target_profile: str
|
|
enabled_toolsets: list[str]
|
|
approval_mode: str
|
|
max_iterations: int
|
|
policy_digest: str
|
|
|
|
|
|
class RoomLinkEndpoint(Result):
|
|
"""``GatewayRoomCatalog.endpoint_mapping``: ``url``/``transport_security`` when available,
|
|
``reason`` when not."""
|
|
|
|
available: bool
|
|
url: str | None = None
|
|
transport_security: str | None = None
|
|
reason: str | None = None
|
|
|
|
|
|
class RoomLinkCatalog(Result):
|
|
"""``gateway/hosted_room_peer.py::GatewayRoomCatalog.as_mapping``."""
|
|
|
|
installation_id: str
|
|
protocol_versions: list[int]
|
|
link_modes: list[str]
|
|
persistent_process: bool
|
|
text: bool
|
|
attachments: bool
|
|
execution_policy: RoomExecutionPolicy
|
|
catalog_digest: str
|
|
endpoint: RoomLinkEndpoint | None = None
|
|
|
|
|
|
class RoomLinkStatus(Result):
|
|
"""``enabled`` with ``profile``/``catalog``/``endpoint``, or disabled with a ``reason``."""
|
|
|
|
enabled: bool
|
|
profile: str | None = None
|
|
catalog: RoomLinkCatalog | None = None
|
|
endpoint: RoomLinkEndpoint | None = None
|
|
reason: str | None = None
|
|
|
|
|
|
# ── groups.capabilities ───────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class GroupsCapabilitiesParams(ProfileParams):
|
|
pass
|
|
|
|
|
|
class GroupsCapabilitiesResult(Result):
|
|
protocol_version: int
|
|
driver: bool
|
|
persistent_process: bool
|
|
authority_gateway_id: str
|
|
room_link: RoomLinkStatus
|
|
features: list[str]
|
|
methods: list[str]
|
|
max_log_limit: int
|
|
|
|
|
|
method("groups.capabilities", params=GroupsCapabilitiesParams, result=GroupsCapabilitiesResult,
|
|
doc="Describe the hosted-room protocol implemented by this gateway.")
|
|
|
|
|
|
# ── groups.list / create / state ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
class GroupsListParams(ProfileParams):
|
|
include_disbanded: bool | None = None
|
|
limit: int | None = None
|
|
offset: int | None = None
|
|
|
|
|
|
class GroupsListResult(Result):
|
|
rooms: list[Room]
|
|
next_offset: int | None = None
|
|
|
|
|
|
method("groups.list", params=GroupsListParams, result=GroupsListResult,
|
|
doc="List rooms hosted by this gateway, most recently changed first.")
|
|
|
|
|
|
class GroupsCreateParams(ProfileParams):
|
|
room_id: str
|
|
name: str
|
|
members: list[RoomMemberInput]
|
|
# Ignored: authority is always this gateway's install identity (a client cannot spoof it).
|
|
authority_gateway_id: str | None = None
|
|
|
|
|
|
class GroupsCreateResult(Result):
|
|
room: Room
|
|
|
|
|
|
method("groups.create", params=GroupsCreateParams, result=GroupsCreateResult,
|
|
doc="Create a hosted room idempotently; authority is this gateway's stable install identity.")
|
|
|
|
|
|
class GroupsStateParams(RoomParams):
|
|
include_disbanded: bool | None = None
|
|
|
|
|
|
class PeerRouteStatus(Result):
|
|
room_id: str
|
|
member_id: str
|
|
status: str
|
|
|
|
|
|
class RoomDriverStatus(Result):
|
|
"""``HostedRoomService.status(room_id)``; ``pending_actions`` rows are ``{kind: retry, task_id}``
|
|
or the driver's approval action (``kind: approval`` + run/session/approval context)."""
|
|
|
|
running: bool
|
|
working: bool
|
|
blocked: bool
|
|
counts: dict[str, int]
|
|
pending_actions: list[dict[str, JsonValue]]
|
|
peer_routes: list[PeerRouteStatus]
|
|
|
|
|
|
class GroupsStateResult(Result):
|
|
room: Room
|
|
driver_status: RoomDriverStatus | None = None
|
|
|
|
|
|
method("groups.state", params=GroupsStateParams, result=GroupsStateResult,
|
|
doc="One hosted room's replay cursor and fenced authority state, plus live driver status.")
|
|
|
|
|
|
# ── groups.send / rename / log ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class GroupsSendParams(RoomParams):
|
|
event_id: str | None = None
|
|
payload: dict[str, JsonValue]
|
|
|
|
|
|
class GroupsSendResult(Result):
|
|
event: RoomEvent
|
|
client_event_id: str | None = None
|
|
accepted: bool = True
|
|
driver_started: bool = True
|
|
|
|
|
|
method("groups.send", params=GroupsSendParams, result=GroupsSendResult,
|
|
doc="Append one inert message.user event idempotently; the actor is server-owned.")
|
|
|
|
|
|
class GroupsRenameParams(RoomParams):
|
|
event_id: str
|
|
name: str
|
|
|
|
|
|
class GroupsRenameResult(Result):
|
|
room: Room
|
|
|
|
|
|
method("groups.rename", params=GroupsRenameParams, result=GroupsRenameResult,
|
|
doc="Rename one hosted room atomically with its replay event.")
|
|
|
|
|
|
class GroupsLogParams(RoomParams):
|
|
since_seq: int | None = None
|
|
limit: int | None = None
|
|
include_disbanded: bool | None = None
|
|
|
|
|
|
class GroupsLogResult(Result):
|
|
"""``gateway/hosted_rooms.py::read_events`` page — also the ``page`` ``groups.replicate`` ingests."""
|
|
|
|
events: list[RoomEvent]
|
|
cursor: int
|
|
latest_seq: int
|
|
has_more: bool
|
|
authority: RoomAuthority
|
|
|
|
|
|
method("groups.log", params=GroupsLogParams, result=GroupsLogResult,
|
|
doc="A monotonic room-log delta after since_seq, bounded by count and page bytes.")
|
|
|
|
|
|
# ── groups.disband / stop / approve / retry ───────────────────────────────────────────────────
|
|
|
|
|
|
class GroupsDisbandParams(RoomParams):
|
|
cancel_id: str | None = None
|
|
|
|
|
|
class RoomTombstone(Result):
|
|
room_id: str
|
|
disbanded_at: float
|
|
idempotent: bool
|
|
history_expired: bool | None = None
|
|
event: RoomEvent | None = None
|
|
|
|
|
|
class GroupsDisbandResult(Result):
|
|
tombstone: RoomTombstone
|
|
|
|
|
|
method("groups.disband", params=GroupsDisbandParams, result=GroupsDisbandResult,
|
|
doc="Permanently tombstone a hosted room id after stopping its work and revoking peer routes.")
|
|
|
|
|
|
class GroupsStopParams(RoomParams):
|
|
cancel_id: str | None = None
|
|
|
|
|
|
class GroupsStopResult(Result):
|
|
cancelled: int
|
|
|
|
|
|
method("groups.stop", params=GroupsStopParams, result=GroupsStopResult,
|
|
doc="Durably cancel queued or running work for one hosted room.")
|
|
|
|
|
|
class GroupsApproveParams(RoomParams):
|
|
member_id: str
|
|
task_id: str
|
|
execution_generation: int
|
|
choice: ApprovalChoice
|
|
request_id: str
|
|
|
|
|
|
class GroupsApproveResult(Result):
|
|
"""``result`` is the local ``approval.respond`` answer or the peer's run-action receipt."""
|
|
|
|
approved: bool = True
|
|
result: dict[str, JsonValue]
|
|
|
|
|
|
method("groups.approve", params=GroupsApproveParams, result=GroupsApproveResult,
|
|
doc="Resolve one exact pending approval raised by a local or peer room member.")
|
|
|
|
|
|
class GroupsRetryParams(RoomParams):
|
|
task_id: str
|
|
|
|
|
|
class RoomTaskReceipt(Result):
|
|
room_id: str
|
|
task_id: str
|
|
thread_id: str
|
|
turn_id: str
|
|
status: str
|
|
execution_generation: int
|
|
cancel_generation: int
|
|
|
|
|
|
class GroupsRetryResult(Result):
|
|
retried: bool = True
|
|
task: RoomTaskReceipt
|
|
|
|
|
|
method("groups.retry", params=GroupsRetryParams, result=GroupsRetryResult,
|
|
doc="Retry one indeterminate room task after explicit user confirmation.")
|
|
|
|
|
|
# ── replication / authority takeover ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class GroupsReplicateParams(RoomParams):
|
|
room_name: str
|
|
members: list[RoomMemberInput]
|
|
page: dict[str, JsonValue] # a verbatim ``groups.log`` result
|
|
|
|
|
|
class GroupsReplicateResult(Result):
|
|
room_id: str
|
|
stored_seq: int
|
|
ingested: int
|
|
authority: RoomAuthority
|
|
caught_up: bool
|
|
|
|
|
|
method("groups.replicate", params=GroupsReplicateParams, result=GroupsReplicateResult,
|
|
doc="Persist one authority-stamped replay page into the local replica store; idempotent.")
|
|
|
|
|
|
class GroupsReplicaStateParams(RoomParams):
|
|
pass
|
|
|
|
|
|
class GroupsReplicaStateResult(Result):
|
|
room_id: str
|
|
name: str
|
|
members: list[RoomMember]
|
|
authority: RoomAuthority
|
|
last_seq: int
|
|
latest_seq: int
|
|
event_bytes: int
|
|
created_at: float
|
|
updated_at: float
|
|
|
|
|
|
method("groups.replica_state", params=GroupsReplicaStateParams, result=GroupsReplicaStateResult,
|
|
doc="The local replica's coverage and authority lineage for one room.")
|
|
|
|
|
|
class GroupsPromoteParams(RoomParams):
|
|
confirm: bool | None = None
|
|
reason: str | None = None
|
|
|
|
|
|
class GroupsPromoteResult(Result):
|
|
room_id: str
|
|
authority_gateway_id: str
|
|
authority_epoch: int
|
|
previous_gateway_id: str
|
|
previous_epoch: int
|
|
claim_seq: int
|
|
latest_seq: int
|
|
|
|
|
|
method("groups.promote", params=GroupsPromoteParams, result=GroupsPromoteResult,
|
|
doc="Continue a replicated room on this gateway at epoch + 1; requires confirm=true.")
|
|
|
|
|
|
class GroupsDemoteParams(RoomParams):
|
|
observed_gateway_id: str
|
|
observed_epoch: int
|
|
|
|
|
|
class GroupsDemoteResult(Result):
|
|
room_id: str
|
|
authority_gateway_id: str
|
|
authority_epoch: int
|
|
idempotent: bool
|
|
|
|
|
|
method("groups.demote", params=GroupsDemoteParams, result=GroupsDemoteResult,
|
|
doc="Fence this gateway's stale room authority against a proven newer epoch.")
|
|
|
|
|
|
# ── peer routes (RoomLink) ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class GroupsPeerInviteParams(ProfileParams):
|
|
room_id: str | None = None
|
|
home_install_id: str | None = None
|
|
authority_gateway_id: str | None = None
|
|
authority_epoch: int | None = None
|
|
member_id: str | None = None
|
|
grant_id: str | None = None
|
|
ttl_seconds: float | None = None
|
|
|
|
|
|
class GroupsPeerInviteResult(Result):
|
|
grant: str
|
|
target_profile: str
|
|
catalog: RoomLinkCatalog
|
|
endpoint: RoomLinkEndpoint
|
|
|
|
|
|
method("groups.peer.invite", params=GroupsPeerInviteParams, result=GroupsPeerInviteResult,
|
|
doc="Mint one target-issued room/profile grant for a prospective room home.")
|
|
|
|
|
|
class GroupsPeerRevokeParams(ProfileParams):
|
|
grant: str
|
|
|
|
|
|
class GroupsPeerRevokeResult(Result):
|
|
revoked: bool = True
|
|
|
|
|
|
method("groups.peer.revoke", params=GroupsPeerRevokeParams, result=GroupsPeerRevokeResult,
|
|
doc="Revoke one target-issued grant using its exact profile scope.")
|
|
|
|
|
|
class GroupsPeerRegisterParams(RoomParams):
|
|
member_id: str
|
|
target_url: str
|
|
target_profile: str
|
|
grant: str
|
|
catalog: dict[str, JsonValue] # a RoomLinkCatalog mapping; ``GatewayRoomCatalog.from_mapping`` is exact
|
|
cancellation_scope_id: str | None = None
|
|
trace_id: str | None = None
|
|
|
|
|
|
class GroupsPeerRegisterResult(Result):
|
|
registered: bool = True
|
|
mode: str
|
|
transport_security: str
|
|
target_install_id: str
|
|
target_profile: str
|
|
|
|
|
|
method("groups.peer.register", params=GroupsPeerRegisterParams, result=GroupsPeerRegisterResult,
|
|
doc="Register and probe one scoped peer route on the room home.")
|
|
|
|
|
|
# ── bot relay ─────────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class RelayAgentRow(Params):
|
|
"""A roster row the Desktop pushes (``tools/bot_relay.py::_normalize_roster_row``); invalid
|
|
rows are dropped server-side, so the shape stays open."""
|
|
|
|
profile: str | None = None
|
|
handle: str | None = None
|
|
connection_id: str | None = None
|
|
connection_label: str | None = None
|
|
title: str | None = None
|
|
description: str | None = None
|
|
online: bool | None = None
|
|
model_config = Params.model_config | {"extra": "allow"}
|
|
|
|
|
|
class BotRelayRosterSyncParams(ProfileParams):
|
|
agents: list[RelayAgentRow] | None = None
|
|
|
|
|
|
class BotRelayRosterSyncResult(Result):
|
|
count: int
|
|
|
|
|
|
method("bot_relay.roster.sync", params=BotRelayRosterSyncParams, result=BotRelayRosterSyncResult,
|
|
doc="Replace this gateway's view of agents on other connections; answers the accepted row count.")
|
|
|
|
|
|
class BotRelayOutboxDrainParams(ProfileParams):
|
|
pass
|
|
|
|
|
|
class RelayEnvelope(OpenModel):
|
|
"""``tools/bot_relay.py::enqueue_envelope``."""
|
|
|
|
id: str
|
|
created_at: int | float
|
|
from_profile: str
|
|
from_handle: str
|
|
target_connection: str
|
|
target_profile: str
|
|
target_handle: str
|
|
message: str
|
|
|
|
|
|
class BotRelayOutboxDrainResult(Result):
|
|
envelopes: list[RelayEnvelope]
|
|
|
|
|
|
method("bot_relay.outbox.drain", params=BotRelayOutboxDrainParams, result=BotRelayOutboxDrainResult,
|
|
doc="Atomically claim every pending cross-connection envelope queued on this gateway.")
|
|
|
|
|
|
class BotRelayDeliverParams(Params):
|
|
"""``profile`` here is the TARGET profile on this gateway (also what the desktop route wrapper adds)."""
|
|
|
|
profile: str
|
|
message: str
|
|
from_profile: str | None = None
|
|
from_handle: str | None = None
|
|
from_connection: str | None = None
|
|
|
|
|
|
class BotRelayDeliverResult(Result):
|
|
reply: str
|
|
|
|
|
|
method("bot_relay.deliver", params=BotRelayDeliverParams, result=BotRelayDeliverResult,
|
|
doc="Deliver a relayed DM into a Bot Chat on this gateway and return the one-turn reply (blocking).")
|
|
|
|
|
|
class BotRelayReplyParams(ProfileParams):
|
|
id: str
|
|
reply: str | None = None
|
|
error: str | None = None
|
|
reason: str | None = None
|
|
|
|
|
|
method("bot_relay.reply", params=BotRelayReplyParams, result=OkResult,
|
|
doc="Write a relayed reply and/or typed error for an envelope so the sender-side waiter resolves.")
|
|
|
|
|
|
# ── browser controller ────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class BrowserControllerParams(Params):
|
|
"""Every controller call names the session the controller is attached to."""
|
|
|
|
session_id: str
|
|
|
|
|
|
class BrowserControllerRegisterParams(BrowserControllerParams):
|
|
controller_id: str
|
|
browser_profile_id: str
|
|
capabilities: list[str] | None = None
|
|
protocol_version: JsonValue | None = None # checked exactly by the handler (an int today)
|
|
# Ignored: the principal is derived from the server-minted identity, never client-supplied.
|
|
principal_id: str | None = None
|
|
|
|
|
|
class ControllerScope(Result):
|
|
principal_id: str
|
|
profile_id: str
|
|
session_id: str
|
|
controller_id: str
|
|
browser_profile_id: str
|
|
transport_family: str
|
|
capabilities: list[str]
|
|
|
|
|
|
class BrowserControllerRegisterResult(Result):
|
|
scope: ControllerScope
|
|
|
|
|
|
method("browser.controller.register", params=BrowserControllerRegisterParams,
|
|
result=BrowserControllerRegisterResult,
|
|
doc="Attach this connection as the browser controller for one session; fails closed (4403).")
|
|
|
|
|
|
class BrowserControllerResultParams(BrowserControllerParams):
|
|
command_id: str
|
|
ok: JsonValue | None = None # only the exact ``true`` counts as success
|
|
result: JsonValue | None = None
|
|
error: JsonValue | None = None
|
|
|
|
|
|
class BrowserControllerResultResult(Result):
|
|
accepted: bool
|
|
|
|
|
|
method("browser.controller.result", params=BrowserControllerResultParams,
|
|
result=BrowserControllerResultResult,
|
|
doc="Deliver one command result to the broker; accepted is false for unknown or settled command ids.")
|
|
|
|
|
|
method("browser.controller.heartbeat", params=BrowserControllerParams, result=OkResult,
|
|
doc="Acknowledge a heartbeat only for this transport's own attached controller.")
|
|
|
|
|
|
class BrowserControllerDetachResult(Result):
|
|
detached: bool = True
|
|
|
|
|
|
method("browser.controller.detach", params=BrowserControllerParams, result=BrowserControllerDetachResult,
|
|
doc="Hard-detach only the controller owned by this authenticated transport.")
|
|
|
|
|
|
__all__ = [
|
|
"GroupsLogResult", "RelayEnvelope", "Room", "RoomAuthority", "RoomEvent", "RoomLinkCatalog",
|
|
"RoomMember", "RoomMemberInput",
|
|
]
|