From 1f8a83d4f899c879caccfa08a933a0ce45ae1946 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Fri, 8 May 2026 13:26:30 -0700 Subject: [PATCH] feat: support allowed repos in addition to allowed orgs for webhook filtering (#1092) * feat: add ALLOWED_GITHUB_REPOS env var for owner/repo-level webhook filtering Previously only org-level filtering was supported via ALLOWED_GITHUB_ORGS. This adds ALLOWED_GITHUB_REPOS for finer-grained control, allowing specific owner/repo pairs to be allowlisted independently of org membership. * fix: update test patches for _is_repo_org_allowed rename --------- Co-authored-by: open-swe[bot] Co-authored-by: Johannes du Plessis --- INSTALLATION.md | 19 +++++++++++---- agent/webapp.py | 45 +++++++++++++++++++++++------------ tests/test_repo_extraction.py | 4 ++-- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index f761a129..935a1ee2 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -213,13 +213,18 @@ GITHUB_USER_EMAIL_MAP = { } ``` -You should also add the GitHub organization which should be allowed to be triggered from in GitHub: +You should also configure which GitHub organizations and/or repositories the agent is allowed to operate on. You can specify allowed orgs, specific `owner/repo` pairs, or both: -`agent/webapp.py` -```python -ALLOWED_GITHUB_ORGS = "langchain-ai,anthropics" +```bash +# Allow all repos in these orgs +ALLOWED_GITHUB_ORGS="langchain-ai,anthropics" + +# Allow specific repos (owner/repo format) +ALLOWED_GITHUB_REPOS="some-user/their-repo,another-org/specific-repo" ``` +A webhook is accepted if the repo's org is in `ALLOWED_GITHUB_ORGS` **or** the `owner/repo` is in `ALLOWED_GITHUB_REPOS`. If both are empty, all repos are allowed. + ### Linear (optional) Open SWE listens for Linear comments that mention `@openswe`. @@ -376,10 +381,14 @@ GITHUB_WEBHOOK_SECRET="" # The secret you generated in step 3b # With these, each user authenticates with their own GitHub account. GITHUB_OAUTH_PROVIDER_ID="" # The provider ID from steps 3a / 4b -# === Org Allowlist (optional) === +# === Repo Allowlist (optional) === # Comma-separated list of GitHub orgs the agent is allowed to operate on. # Leave empty to allow all orgs. ALLOWED_GITHUB_ORGS="" # e.g. "my-org,my-other-org" +# Comma-separated list of specific owner/repo pairs the agent is allowed to operate on. +# A repo is allowed if its org is in ALLOWED_GITHUB_ORGS OR its owner/repo is in ALLOWED_GITHUB_REPOS. +# Leave both empty to allow all repos. +ALLOWED_GITHUB_REPOS="" # e.g. "some-user/their-repo,another-org/specific-repo" # === Default Repository === # Used across all triggers when no repo is specified. diff --git a/agent/webapp.py b/agent/webapp.py index 37df66d7..8b923cb8 100644 --- a/agent/webapp.py +++ b/agent/webapp.py @@ -120,6 +120,12 @@ ALLOWED_REVIEWER_GITHUB_REPOS: frozenset[str] = frozenset( # the public-repo gate is disabled (back-compat). PUBLIC_REPO_ORG_GATE: str = os.environ.get("PUBLIC_REPO_ORG_GATE", "").strip() +ALLOWED_GITHUB_REPOS: frozenset[str] = frozenset( + repo.strip().lower() + for repo in os.environ.get("ALLOWED_GITHUB_REPOS", "").split(",") + if repo.strip() +) + LINEAR_API_KEY = os.environ.get("LINEAR_API_KEY", "") _GITHUB_BOT_MESSAGE_PREFIXES = ( @@ -341,16 +347,22 @@ def _run_id_for_logging(run: Any) -> str: return run_id if isinstance(run_id, str) and run_id else "" -def _is_repo_org_allowed(repo_config: dict[str, str]) -> bool: - """Check if the repo owner/org is in the allowlist. +def _is_repo_allowed(repo_config: dict[str, str]) -> bool: + """Check if the repo is in the allowlist. - Returns True if no allowlist is configured (empty ALLOWED_GITHUB_ORGS), - or if the repo owner is in the allowlist. + Returns True if no allowlist is configured (both ALLOWED_GITHUB_ORGS and + ALLOWED_GITHUB_REPOS are empty), or if the repo owner is in + ALLOWED_GITHUB_ORGS, or if owner/name is in ALLOWED_GITHUB_REPOS. """ - if not ALLOWED_GITHUB_ORGS: + if not ALLOWED_GITHUB_ORGS and not ALLOWED_GITHUB_REPOS: return True owner = repo_config.get("owner", "").lower() - return owner in ALLOWED_GITHUB_ORGS + name = repo_config.get("name", "").lower() + if ALLOWED_GITHUB_ORGS and owner in ALLOWED_GITHUB_ORGS: + return True + if ALLOWED_GITHUB_REPOS and f"{owner}/{name}" in ALLOWED_GITHUB_REPOS: + return True + return False def _is_repo_allowed_for_reviewer(repo_config: dict[str, str]) -> bool: @@ -1117,12 +1129,13 @@ async def linear_webhook( # noqa: PLR0911, PLR0912, PLR0915 }, ) - if not _is_repo_org_allowed(repo_config): + if not _is_repo_allowed(repo_config): logger.warning( - "Rejecting Linear webhook: org '%s' not in ALLOWED_GITHUB_ORGS", + "Rejecting Linear webhook: repo '%s/%s' not in allowlist", repo_config.get("owner"), + repo_config.get("name"), ) - return {"status": "ignored", "reason": "Repository org not in allowlist"} + return {"status": "ignored", "reason": "Repository not in allowlist"} repo_owner = repo_config["owner"] repo_name = repo_config["name"] @@ -1252,12 +1265,13 @@ async def slack_webhook(request: Request, background_tasks: BackgroundTasks) -> } repo_config = await get_slack_repo_config(text, channel_id, thread_ts) - if not _is_repo_org_allowed(repo_config): + if not _is_repo_allowed(repo_config): logger.warning( - "Rejecting Slack webhook: org '%s' not in ALLOWED_GITHUB_ORGS", + "Rejecting Slack webhook: repo '%s/%s' not in allowlist", repo_config.get("owner"), + repo_config.get("name"), ) - return {"status": "ignored", "reason": "Repository org not in allowlist"} + return {"status": "ignored", "reason": "Repository not in allowlist"} background_tasks.add_task(process_slack_mention, event_data, repo_config) @@ -2271,12 +2285,13 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) -> background_tasks.add_task(process_github_push_event, payload) return {"status": "accepted", "message": "Processing GitHub push for reviewer watch"} - if not _is_repo_org_allowed(webhook_repo_config): + if not _is_repo_allowed(webhook_repo_config): logger.warning( - "Rejecting GitHub webhook: org '%s' not in ALLOWED_GITHUB_ORGS", + "Rejecting GitHub webhook: repo '%s/%s' not in allowlist", webhook_repo_config.get("owner"), + webhook_repo_config.get("name"), ) - return {"status": "ignored", "reason": "Repository org not in allowlist"} + return {"status": "ignored", "reason": "Repository not in allowlist"} if is_issue_event: action = payload.get("action", "") diff --git a/tests/test_repo_extraction.py b/tests/test_repo_extraction.py index e663803d..4ea57bd2 100644 --- a/tests/test_repo_extraction.py +++ b/tests/test_repo_extraction.py @@ -92,7 +92,7 @@ class TestLinearWebhookRepoOverride: "comments": {"nodes": []}, }, ), - patch("agent.webapp._is_repo_org_allowed", return_value=True), + patch("agent.webapp._is_repo_allowed", return_value=True), patch("agent.webapp.BackgroundTasks"), ): mock_request = AsyncMock() @@ -142,7 +142,7 @@ class TestLinearWebhookRepoOverride: "comments": {"nodes": []}, }, ), - patch("agent.webapp._is_repo_org_allowed", return_value=True), + patch("agent.webapp._is_repo_allowed", return_value=True), ): mock_request = AsyncMock() mock_request.body.return_value = json.dumps(payload).encode()