mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-04 16:42:35 +00:00
Imported from andrewyng/aisuite@1b4bbf303e (contents of its platform/ directory, hoisted to the repo root). Development history prior to this commit lives in that repository. Co-authored-by: Devika <devikaverma11@gmail.com>
203 lines
8.3 KiB
Python
203 lines
8.3 KiB
Python
"""Managed GitHub relay adapter — the second consumer of the shared relay WS.
|
|
|
|
Inbound `@ocw` mentions / `ocw`-label events arrive as relay frames tagged
|
|
`provider: github` (github-relay-spec §7); the RelayHub fans them here. The
|
|
adapter maps them to MessageEvents with `github:owner/repo#N` addressing —
|
|
`installation_id` rides in `source.team_id`, so the gateway's per-team
|
|
allow-list machinery (park → allow & deliver) works unchanged, keyed by
|
|
installation instead of workspace.
|
|
|
|
Outbound (`send`) posts an issue/PR comment via the GitHub REST API with a
|
|
short-lived installation token from the token client — the reply path of the
|
|
`send_message` tool. Richer writes (reviews) are dedicated tools.
|
|
|
|
Sender identity is simpler than Slack: logins are human-readable and ride in
|
|
the payload, so there are no name-resolution calls at all.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import time
|
|
from typing import Any, Awaitable, Callable, Optional
|
|
|
|
from .base import BasePlatformAdapter, MessageEvent, SendResult, SessionSource
|
|
from .relay_client import RelayHub
|
|
|
|
logger = logging.getLogger("coworker.connectors")
|
|
|
|
# installation_id -> a fresh installation token (memory-only, never at rest).
|
|
TokenClient = Callable[[str], Awaitable[str]]
|
|
|
|
|
|
def split_thread(chat_id: str) -> tuple[str, Optional[int]]:
|
|
"""`owner/repo#N` → ("owner/repo", N); a bare repo has no thread number."""
|
|
repo, _, num = chat_id.partition("#")
|
|
try:
|
|
return repo, int(num) if num else None
|
|
except ValueError:
|
|
return repo, None
|
|
|
|
|
|
class GitHubRelayAdapter(BasePlatformAdapter):
|
|
platform = "github"
|
|
|
|
def __init__(
|
|
self,
|
|
hub: RelayHub,
|
|
*,
|
|
installs: Optional[dict[str, dict[str, Any]]] = None,
|
|
token_client: Optional[TokenClient] = None,
|
|
) -> None:
|
|
super().__init__()
|
|
self._hub = hub
|
|
# installation_id -> {account_login, github_login, repo_selection}.
|
|
# Mutable: a `revoked` frame drops one, an install hot-reload adds one.
|
|
self._installs: dict[str, dict[str, Any]] = dict(installs or {})
|
|
self._token_client = token_client
|
|
# owner/repo -> installation_id, learned from inbound events so replies
|
|
# to a repo mint the right installation's token.
|
|
self._repo_installs: dict[str, str] = {}
|
|
self.last_event_at: Optional[float] = None
|
|
# owner/repo -> events the cloud dropped (offline > TTL / overflow);
|
|
# surfaced via status() — GitHub has no cheap "what did I miss" pull.
|
|
self.missed: dict[str, int] = {}
|
|
|
|
# -- lifecycle -----------------------------------------------------------
|
|
async def connect(self) -> bool:
|
|
self._hub.register(self.platform, self._dispatch)
|
|
ok = await self._hub.start()
|
|
if ok:
|
|
logger.info(
|
|
"github adapter connected (managed relay), %d installation(s)",
|
|
len(self._installs),
|
|
)
|
|
return ok
|
|
|
|
async def disconnect(self) -> None:
|
|
await self._hub.release(self.platform)
|
|
|
|
def status(self) -> dict[str, Any]:
|
|
"""Health snapshot for the GUI: shared-socket state + per-installation
|
|
token health (an installation revoked upstream fails its mints)."""
|
|
return {
|
|
"state": self._hub.state(),
|
|
"reconnects": self._hub.reconnects,
|
|
"last_event_at": self.last_event_at,
|
|
"last_error": self._hub.last_error,
|
|
"installs": {
|
|
iid: {"token_ok": bool(info.get("token_ok", True))}
|
|
for iid, info in self._installs.items()
|
|
},
|
|
"missed": dict(self.missed),
|
|
}
|
|
|
|
# -- installation registry ------------------------------------------------
|
|
def set_install(self, installation_id: str, info: dict[str, Any]) -> None:
|
|
self._installs[installation_id] = dict(info)
|
|
|
|
def _note_token_health(self, installation_id: str, ok: bool) -> None:
|
|
info = self._installs.get(installation_id)
|
|
if info is not None:
|
|
info["token_ok"] = ok
|
|
|
|
# -- frame dispatch --------------------------------------------------------
|
|
async def _dispatch(self, frame: dict) -> None:
|
|
kind = frame.get("kind")
|
|
if kind == "missed":
|
|
repo = frame.get("channel", "")
|
|
self.missed[repo] = self.missed.get(repo, 0) + int(
|
|
frame.get("count", 0) or 1
|
|
)
|
|
logger.info(
|
|
"github relay: %s event(s) missed in %s", frame.get("count"), repo
|
|
)
|
|
return
|
|
if kind == "revoked":
|
|
self._installs.pop(str(frame.get("installation_id", "")), None)
|
|
logger.info(
|
|
"github relay installation %s revoked — dropped",
|
|
frame.get("installation_id"),
|
|
)
|
|
return
|
|
await self._on_event(frame)
|
|
|
|
async def _on_event(self, frame: dict) -> None:
|
|
"""A routed trigger (mention / label). Senders are logins — readable as
|
|
they are, no resolution round-trips."""
|
|
self.last_event_at = time.time()
|
|
installation_id = str(frame.get("installation_id", ""))
|
|
owner_repo = frame.get("owner_repo", "")
|
|
number = frame.get("number", "")
|
|
if not owner_repo:
|
|
return
|
|
if installation_id:
|
|
self._repo_installs[owner_repo] = installation_id
|
|
chat_id = f"{owner_repo}#{number}" if number else owner_repo
|
|
title = frame.get("title", "")
|
|
body = frame.get("body", "")
|
|
kind = frame.get("kind", "mention")
|
|
header = f"[{kind} in {owner_repo}#{number}" + (f": {title}]" if title else "]")
|
|
event = MessageEvent(
|
|
text=f"{header} {body}".strip(),
|
|
source=SessionSource(
|
|
platform=self.platform,
|
|
chat_id=chat_id,
|
|
user_id=frame.get("sender", ""),
|
|
user_name=frame.get("sender", ""),
|
|
chat_name=chat_id,
|
|
chat_type="channel", # a repo thread is a channel, not a DM
|
|
team_id=installation_id, # the allow-list scope (≙ Slack team)
|
|
),
|
|
raw=frame,
|
|
)
|
|
await self.handle_message(event)
|
|
|
|
# -- outbound --------------------------------------------------------------
|
|
async def send(
|
|
self, chat_id: str, text: str, *, thread_id: Optional[str] = None
|
|
) -> SendResult:
|
|
"""Comment on the issue/PR the event came from, as `ocw[bot]`."""
|
|
owner_repo, number = split_thread(chat_id)
|
|
if number is None:
|
|
return SendResult(False, error=f"no issue/PR number in {chat_id!r}")
|
|
installation_id = self._repo_installs.get(owner_repo) or next(
|
|
iter(self._installs), ""
|
|
)
|
|
if not (self._token_client and installation_id):
|
|
return SendResult(False, error="no installation token available")
|
|
try:
|
|
token = await self._token_client(installation_id)
|
|
except Exception as exc:
|
|
self._note_token_health(installation_id, False)
|
|
return SendResult(False, error=f"token mint failed: {exc}")
|
|
if not token:
|
|
self._note_token_health(installation_id, False)
|
|
return SendResult(False, error="token mint failed")
|
|
|
|
import httpx
|
|
|
|
base = os.environ.get("GITHUB_API_URL", "https://api.github.com").rstrip("/")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=20) as http:
|
|
resp = await http.post(
|
|
f"{base}/repos/{owner_repo}/issues/{number}/comments",
|
|
json={"body": text},
|
|
headers={
|
|
"Authorization": f"Bearer {token}",
|
|
"Accept": "application/vnd.github+json",
|
|
},
|
|
)
|
|
except httpx.HTTPError as exc:
|
|
return SendResult(False, error=f"github unreachable: {type(exc).__name__}")
|
|
if resp.status_code == 401:
|
|
self._note_token_health(installation_id, False)
|
|
return SendResult(False, error="installation token rejected")
|
|
if resp.status_code not in (200, 201):
|
|
return SendResult(
|
|
False, error=f"github comment failed ({resp.status_code})"
|
|
)
|
|
self._note_token_health(installation_id, True)
|
|
return SendResult(True, message_id=str((resp.json() or {}).get("id", "")))
|