mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
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] <open-swe@users.noreply.github.com> Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
This commit is contained in:
parent
37c194dc6d
commit
1f8a83d4f8
3 changed files with 46 additions and 22 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 "<unknown>"
|
||||
|
||||
|
||||
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", "")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue