"""Managed GitHub relay, desktop side (github-relay-spec §13 Step 3, MG3a). Install callback → per-installation profiles (metadata only, NO tokens at rest), the shared-hub adapter (fan-out by provider tag), the memory-only installation-token client, and tool auth resolution (minted token for managed, PAT untouched for manual). Hermetic: fake transports, stubbed broker calls. """ from __future__ import annotations import asyncio import json import pytest from fastapi.testclient import TestClient from coworker import cloud from coworker.connectors import github_installs from coworker.connectors.base import MessageEvent from coworker.connectors.config import is_authorized, load_settings from coworker.connectors.github_relay import GitHubRelayAdapter, split_thread from coworker.connectors.relay_client import RelayHub, SlackRelayAdapter from coworker.secrets import SecretStore from coworker.server import SessionManager, create_app @pytest.fixture(autouse=True) def _fresh_token_cache(): cloud._GITHUB_TOKEN_CACHE.clear() yield cloud._GITHUB_TOKEN_CACHE.clear() @pytest.fixture def client(tmp_path, monkeypatch): monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) manager = SessionManager(workspace=tmp_path) app = create_app(manager) with TestClient(app) as c: c.manager = manager yield c def _install_form(installation_id: str, *, login="octocat", account="acme") -> dict: """The broker's loopback POST — deliberately NO token fields (§4).""" state = f"github-{installation_id}" cloud._pending_managed_states[state] = cloud._now() return { "connector": "github", "installation_id": installation_id, "account_login": account, "account_type": "Organization", "github_login": login, "repo_selection": "selected", "connection_id": f"conn_{installation_id}", "app_state": state, } def _no_cloud(monkeypatch): calls: list[str] = [] monkeypatch.setattr( cloud, "github_disconnect_installation", lambda s, c, installation_id: calls.append(installation_id), ) return calls # --- install callback → profiles (mirror of the Slack workspace tests) -------- def test_managed_callback_installs_and_hot_reloads(client, monkeypatch): refreshes = [] async def _refresh(): refreshes.append(True) return [] monkeypatch.setattr(client.manager, "refresh_gateway", _refresh) resp = client.post("/oauth/callback", data=_install_form("101")) assert resp.status_code == 200 and "GitHub connected" in resp.text profile = client.manager.secrets.get("github:install:101") assert profile["account_login"] == "acme" assert profile["github_login"] == "octocat" assert profile["repo_selection"] == "selected" # No token of any shape at rest — installation tokens are minted on demand. blob = json.dumps(profile) assert "ghs_" not in blob and "ghu_" not in blob and "token" not in blob assert client.manager.secrets.get("github:default")["mode"] == "relay" assert refreshes # hot-add, like a Slack workspace gh = [ c for c in client.get("/v1/connectors").json()["connectors"] if c["name"] == "github" ][0] assert gh["connected"] is True assert [i["installation_id"] for i in gh["installations"]] == ["101"] assert gh["installations"][0]["account_login"] == "acme" # a second installation lands alongside, not instead client.post("/oauth/callback", data=_install_form("202", account="hooli")) assert client.manager.secrets.get("github:install:101") is not None assert client.manager.secrets.get("github:install:202") is not None def test_disconnect_one_installation_keeps_the_other(client, monkeypatch): cloud_calls = _no_cloud(monkeypatch) for iid in ("101", "202"): client.post("/oauth/callback", data=_install_form(iid)) body = client.post("/v1/connectors/github/installations/101/disconnect").json() assert body["ok"] is True and body["remaining_installs"] == 1 assert cloud_calls == ["101"] assert client.manager.secrets.get("github:install:101") is None assert client.manager.secrets.get("github:install:202") is not None gh = next(c for c in client.manager.list_connectors() if c["name"] == "github") assert gh["connected"] is True assert [i["installation_id"] for i in gh["installations"]] == ["202"] def test_disconnect_last_installation_never_resurrects_manual_pat(client, monkeypatch): _no_cloud(monkeypatch) # A manual PAT stored BEFORE the managed install must stay stored but the # relay must not survive the last installation's removal. client.manager.secrets.put( "github:default", {"type": "token", "token": "ghp_manual", "enabled": True} ) client.post("/oauth/callback", data=_install_form("101")) body = client.post("/v1/connectors/github/installations/101/disconnect").json() assert body["ok"] is True and body["remaining_installs"] == 0 default = client.manager.secrets.get("github:default") assert default["token"] == "ghp_manual" # kept for a manual re-enable assert default["enabled"] is False and "mode" not in default # --- allow-list: per-installation scope (the per-workspace pattern) ----------- def test_github_settings_carry_per_installation_allowlists(tmp_path, monkeypatch): monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) secrets = SecretStore() github_installs.managed_connect_install(secrets, _install_form("101")) secrets.put( "github:install:101", {**secrets.get("github:install:101"), "allowed_users": ["octocat"]}, ) settings = load_settings(secrets)["github"] assert settings.enabled is True class Src: platform = "github" team_id = "101" user_id = "octocat" assert is_authorized(settings, Src()) is True Src.user_id = "stranger" assert is_authorized(settings, Src()) is False # parks, not delivered Src.team_id = "999" assert is_authorized(settings, Src()) is False # unknown installation # --- adapter: shared hub fan-out, dispatch, lifecycle frames ------------------- class FakeTransport: def __init__(self, frames): self._q: asyncio.Queue = asyncio.Queue() for f in frames: self._q.put_nowait(f) async def open(self): pass async def recv(self): if not self._q.empty(): return self._q.get_nowait() await asyncio.Event().wait() async def close(self): pass def _gh_frame(**over): frame = { "provider": "github", "installation_id": "101", "owner_repo": "acme/site", "number": 7, "kind": "mention", "sender": "octocat", "title": "Broken build", "body": "@ocw please take a look", "url": "https://github.com/acme/site/issues/7", "address": "github:acme/site#7", "event_id": "d-1", } frame.update(over) return frame def _slack_frame(): return { "provider": "slack", "team_id": "T1", "channel": "C1", "event_id": "Ev1", "event": {"type": "app_mention", "user": "U_A", "channel": "C1", "text": "hi"}, } async def test_one_hub_fans_out_to_both_adapters(monkeypatch): """THE step-3 invariant: slack + github share one relay socket; frames land on their own adapter by provider tag.""" monkeypatch.setenv("SLACK_API_URL", "http://127.0.0.1:9/") hub = RelayHub( "wss://relay.test/ws", lambda: "jwt", transport_factory=lambda: FakeTransport([_gh_frame(), _slack_frame()]), ) slack = SlackRelayAdapter( "wss://relay.test/ws", lambda: "jwt", teams={"T1": {"bot_token": "xoxb-1", "bot_user_id": "UBOT"}}, hub=hub, ) github = GitHubRelayAdapter(hub, installs={"101": {"account_login": "acme"}}) slack_events: list[MessageEvent] = [] github_events: list[MessageEvent] = [] async def on_slack(e): slack_events.append(e) async def on_github(e): github_events.append(e) slack.set_message_handler(on_slack) github.set_message_handler(on_github) assert await slack.connect() is True assert await github.connect() is True # joins the running socket try: await hub.wait_dispatched(2) finally: await github.disconnect() await slack.disconnect() assert len(slack_events) == 1 and slack_events[0].source.platform == "slack" assert len(github_events) == 1 gh = github_events[0] assert gh.source.platform == "github" assert gh.source.chat_id == "acme/site#7" assert gh.source.target == "github:acme/site#7" # reply handle roundtrip assert gh.source.team_id == "101" # allow-list scope = installation assert gh.source.user_id == "octocat" assert "Broken build" in gh.text and "@ocw please take a look" in gh.text async def test_adapter_missed_and_revoked_frames(): hub = RelayHub( "wss://x", lambda: "jwt", transport_factory=lambda: FakeTransport( [ { "provider": "github", "kind": "missed", "channel": "acme/site", "count": 3, }, {"provider": "github", "kind": "revoked", "installation_id": "101"}, ] ), ) adapter = GitHubRelayAdapter(hub, installs={"101": {"account_login": "acme"}}) await adapter.connect() try: await hub.wait_dispatched(2) finally: await adapter.disconnect() assert adapter.missed == {"acme/site": 3} assert adapter.status()["installs"] == {} # revoked → dropped def test_addressing_roundtrip(): assert split_thread("acme/site#7") == ("acme/site", 7) assert split_thread("acme/site") == ("acme/site", None) async def test_send_posts_comment_with_minted_token(monkeypatch): """Replies mint the right installation's token and post as the bot.""" hub = RelayHub( "wss://x", lambda: "jwt", transport_factory=lambda: FakeTransport([]) ) minted: list[str] = [] async def token_client(installation_id: str) -> str: minted.append(installation_id) return "ghs_live-token" adapter = GitHubRelayAdapter( hub, installs={"101": {"account_login": "acme"}}, token_client=token_client ) adapter._repo_installs["acme/site"] = "101" posted = {} class FakeResp: status_code = 201 def json(self): return {"id": 987} class FakeClient: def __init__(self, **kw): pass async def __aenter__(self): return self async def __aexit__(self, *a): return False async def post(self, url, **kw): posted.update({"url": url, **kw}) return FakeResp() import httpx monkeypatch.setattr(httpx, "AsyncClient", FakeClient) result = await adapter.send("acme/site#7", "on it — as ocw[bot]") assert result.ok and result.message_id == "987" assert minted == ["101"] assert posted["url"].endswith("/repos/acme/site/issues/7/comments") assert posted["json"] == {"body": "on it — as ocw[bot]"} assert posted["headers"]["Authorization"] == "Bearer ghs_live-token" # --- token client: memory cache + re-mint (fake broker) ------------------------ def _stub_broker_mint(monkeypatch, tokens: list[str]): """cloud.github_installation_token's HTTP leg, one token per mint call.""" calls = [] it = iter(tokens) class Resp: status_code = 200 def json(self): return {"token": next(it), "expires_at": "2099-01-01T00:00:00Z"} def fake_post(url, **kw): calls.append(url) assert url.endswith("/v1/github/token") return Resp() monkeypatch.setattr(cloud.httpx, "post", fake_post) monkeypatch.setattr(cloud, "fresh_access_token", lambda s, c: "signin-jwt") return calls def test_token_client_caches_and_force_remints(tmp_path, monkeypatch): monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) from coworker.config import load_config secrets = SecretStore() calls = _stub_broker_mint(monkeypatch, ["ghs_first", "ghs_second"]) t1 = cloud.github_installation_token(secrets, load_config(), "101") t2 = cloud.github_installation_token(secrets, load_config(), "101") assert t1 == t2 == "ghs_first" assert len(calls) == 1 # cache hit, no second mint t3 = cloud.github_installation_token(secrets, load_config(), "101", force=True) assert t3 == "ghs_second" and len(calls) == 2 # NEVER at rest: nothing github-token-shaped in the secret store. blob = json.dumps([m for m in secrets.status()]) assert "ghs_" not in blob # --- tools: minted token for managed, PAT untouched for manual ----------------- def _capture_requests(monkeypatch): from coworker.connectors import integration_tools seen: list[dict] = [] def fake_request(method, url, *, headers=None, params=None, json=None, auth=None): seen.append({"method": method, "url": url, "headers": headers or {}}) return {"ok": True, "data": {"items": []}} monkeypatch.setattr(integration_tools, "_request", fake_request) return seen def _tool(secrets, name): from coworker.connectors.integration_tools import make_integration_tools tools = make_integration_tools(secrets) return next(t for t in tools if t.__name__ == name) def test_manual_pat_path_untouched(tmp_path, monkeypatch): monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) secrets = SecretStore() secrets.put("github:default", {"type": "token", "token": "ghp_manual"}) seen = _capture_requests(monkeypatch) out = _tool(secrets, "github_get_issue")("acme", "site", 7) assert out["ok"] is True assert seen[0]["headers"]["Authorization"] == "Bearer ghp_manual" def test_managed_tools_use_minted_token_by_owner(tmp_path, monkeypatch): monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) secrets = SecretStore() github_installs.managed_connect_install(secrets, _install_form("101")) github_installs.managed_connect_install( secrets, _install_form("202", account="hooli") ) seen = _capture_requests(monkeypatch) minted = [] def fake_mint(s, c, installation_id, *, force=False): minted.append((installation_id, force)) return f"ghs_for-{installation_id}" monkeypatch.setattr(cloud, "github_installation_token", fake_mint) # The repo owner picks the installation (hooli ≠ the default 101). out = _tool(secrets, "github_reply")("hooli", "app", 3, "done") assert out["ok"] is True assert minted == [("202", False)] assert seen[0]["headers"]["Authorization"] == "Bearer ghs_for-202" assert seen[0]["url"].endswith("/repos/hooli/app/issues/3/comments") def test_managed_401_reminted_once(tmp_path, monkeypatch): monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) from coworker.connectors import integration_tools secrets = SecretStore() github_installs.managed_connect_install(secrets, _install_form("101")) minted = [] monkeypatch.setattr( cloud, "github_installation_token", lambda s, c, iid, *, force=False: minted.append(force) or "ghs_x", ) responses = iter([{"error": "HTTP 401"}, {"ok": True, "data": {}}]) monkeypatch.setattr(integration_tools, "_request", lambda *a, **k: next(responses)) out = _tool(secrets, "github_review")("acme", "site", 5, "APPROVE") assert out["ok"] is True assert minted == [False, True] # expired cache → one forced re-mint def test_review_event_validated(tmp_path, monkeypatch): monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) secrets = SecretStore() out = _tool(secrets, "github_review")("acme", "site", 5, "MERGE") assert "event must be" in out["error"] # --- commits + clone/pull (activity summaries + local code exploration) -------- def test_list_commits_filters_and_trims(tmp_path, monkeypatch): monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) from coworker.connectors import integration_tools secrets = SecretStore() secrets.put("github:default", {"type": "token", "token": "ghp_x"}) seen = {} def fake_request(method, url, *, headers=None, params=None, json=None, auth=None): seen.update({"url": url, "params": params}) return { "ok": True, "data": [ { "sha": "a" * 40, "commit": { "author": {"name": "Rohit", "date": "2026-07-08T10:00:00Z"}, "message": "Fix the flaky relay test\n\nlong body " * 40, }, "author": {"login": "rohit-dev"}, } ], } monkeypatch.setattr(integration_tools, "_request", fake_request) out = _tool(secrets, "github_list_commits")( "acme", "site", since="2026-07-06T00:00:00Z", author="rohit-dev", max_results=200, ) assert seen["url"].endswith("/repos/acme/site/commits") assert seen["params"]["since"] == "2026-07-06T00:00:00Z" assert seen["params"]["author"] == "rohit-dev" assert seen["params"]["per_page"] == 100 # capped (c,) = out["commits"] assert c["sha"] == "a" * 12 and c["author"] == "Rohit" assert len(c["message"]) <= 500 # trimmed for the model def _git(args, cwd): import subprocess return subprocess.run( ["git", "-c", "user.name=t", "-c", "user.email=t@t", *args], cwd=cwd, check=True, capture_output=True, text=True, ) @pytest.fixture def _origin(tmp_path): """A local 'GitHub': a bare repo at /acme/site.git reachable via the GITHUB_GIT_URL override, plus a work repo to push new commits from.""" base = tmp_path / "githost" bare = base / "acme" / "site.git" bare.mkdir(parents=True) _git(["init", "--bare", "--initial-branch=main", str(bare)], cwd=tmp_path) work = tmp_path / "work" work.mkdir() _git(["init", "--initial-branch=main"], cwd=work) (work / "README.md").write_text("hello") _git(["add", "."], cwd=work) _git(["commit", "-m", "first"], cwd=work) _git(["remote", "add", "origin", str(bare)], cwd=work) _git(["push", "origin", "main"], cwd=work) return {"base": base, "work": work} def _clone_tools(secrets, tmp_path): from coworker.connectors.integration_tools import make_integration_tools from coworker.roots import RootDir granted = tmp_path / "granted" granted.mkdir(exist_ok=True) tools = make_integration_tools(secrets, roots=[RootDir(granted, writable=True)]) by_name = {t.__name__: t for t in tools} return granted, by_name def test_clone_pull_roundtrip_and_no_token_at_rest(tmp_path, monkeypatch, _origin): """Clone into the granted root, then pull a new upstream commit. The minted-token header must never persist anywhere in the clone.""" monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) monkeypatch.setenv("GITHUB_GIT_URL", f"file://{_origin['base']}") secrets = SecretStore() github_installs.managed_connect_install(secrets, _install_form("101")) monkeypatch.setattr( cloud, "github_installation_token", lambda s, c, iid, *, force=False: "ghs_live" ) granted, tools = _clone_tools(secrets, tmp_path) out = tools["github_clone"]("acme", "site") assert out.get("ok") is True, out clone = granted / "site" assert (clone / "README.md").read_text() == "hello" # The no-token-at-rest rule, verified on disk (not just in code review): for f in (clone / ".git").rglob("*"): if f.is_file() and f.stat().st_size < 100_000: blob = f.read_bytes() assert b"ghs_" not in blob and b"AUTHORIZATION" not in blob, f (_origin["work"] / "next.txt").write_text("more") _git(["add", "."], cwd=_origin["work"]) _git(["commit", "-m", "second"], cwd=_origin["work"]) _git(["push", "origin", "main"], cwd=_origin["work"]) out = tools["github_pull"](str(clone)) assert out.get("ok") is True, out assert (clone / "next.txt").read_text() == "more" def test_clone_refuses_paths_outside_granted_roots(tmp_path, monkeypatch, _origin): monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) monkeypatch.setenv("GITHUB_GIT_URL", f"file://{_origin['base']}") secrets = SecretStore() _granted, tools = _clone_tools(secrets, tmp_path) out = tools["github_clone"]("acme", "site", directory=str(tmp_path / "elsewhere")) assert "outside the session's writable directories" in out["error"] assert not (tmp_path / "elsewhere").exists() # and with no writable root at all → a clear error, no filesystem writes from coworker.connectors.integration_tools import make_integration_tools bare_tools = {t.__name__: t for t in make_integration_tools(secrets, roots=[])} out = bare_tools["github_clone"]("acme", "site") assert "no writable session directory" in out["error"] def test_clone_refuses_non_empty_target(tmp_path, monkeypatch, _origin): monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) monkeypatch.setenv("GITHUB_GIT_URL", f"file://{_origin['base']}") secrets = SecretStore() granted, tools = _clone_tools(secrets, tmp_path) (granted / "site").mkdir() (granted / "site" / "keep.txt").write_text("existing work") out = tools["github_clone"]("acme", "site") assert "not empty" in out["error"] assert (granted / "site" / "keep.txt").read_text() == "existing work"