mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-04 16:42:35 +00:00
OpenWorker: initial import
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>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
"""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", "")))
|
||||
Reference in New Issue
Block a user