mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-03 08:03:28 +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`
|
```bash
|
||||||
```python
|
# Allow all repos in these orgs
|
||||||
ALLOWED_GITHUB_ORGS = "langchain-ai,anthropics"
|
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)
|
### Linear (optional)
|
||||||
|
|
||||||
Open SWE listens for Linear comments that mention `@openswe`.
|
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.
|
# With these, each user authenticates with their own GitHub account.
|
||||||
GITHUB_OAUTH_PROVIDER_ID="" # The provider ID from steps 3a / 4b
|
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.
|
# Comma-separated list of GitHub orgs the agent is allowed to operate on.
|
||||||
# Leave empty to allow all orgs.
|
# Leave empty to allow all orgs.
|
||||||
ALLOWED_GITHUB_ORGS="" # e.g. "my-org,my-other-org"
|
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 ===
|
# === Default Repository ===
|
||||||
# Used across all triggers when no repo is specified.
|
# 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).
|
# the public-repo gate is disabled (back-compat).
|
||||||
PUBLIC_REPO_ORG_GATE: str = os.environ.get("PUBLIC_REPO_ORG_GATE", "").strip()
|
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", "")
|
LINEAR_API_KEY = os.environ.get("LINEAR_API_KEY", "")
|
||||||
|
|
||||||
_GITHUB_BOT_MESSAGE_PREFIXES = (
|
_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>"
|
return run_id if isinstance(run_id, str) and run_id else "<unknown>"
|
||||||
|
|
||||||
|
|
||||||
def _is_repo_org_allowed(repo_config: dict[str, str]) -> bool:
|
def _is_repo_allowed(repo_config: dict[str, str]) -> bool:
|
||||||
"""Check if the repo owner/org is in the allowlist.
|
"""Check if the repo is in the allowlist.
|
||||||
|
|
||||||
Returns True if no allowlist is configured (empty ALLOWED_GITHUB_ORGS),
|
Returns True if no allowlist is configured (both ALLOWED_GITHUB_ORGS and
|
||||||
or if the repo owner is in the allowlist.
|
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
|
return True
|
||||||
owner = repo_config.get("owner", "").lower()
|
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:
|
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(
|
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("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_owner = repo_config["owner"]
|
||||||
repo_name = repo_config["name"]
|
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)
|
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(
|
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("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)
|
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)
|
background_tasks.add_task(process_github_push_event, payload)
|
||||||
return {"status": "accepted", "message": "Processing GitHub push for reviewer watch"}
|
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(
|
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("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:
|
if is_issue_event:
|
||||||
action = payload.get("action", "")
|
action = payload.get("action", "")
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class TestLinearWebhookRepoOverride:
|
||||||
"comments": {"nodes": []},
|
"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"),
|
patch("agent.webapp.BackgroundTasks"),
|
||||||
):
|
):
|
||||||
mock_request = AsyncMock()
|
mock_request = AsyncMock()
|
||||||
|
|
@ -142,7 +142,7 @@ class TestLinearWebhookRepoOverride:
|
||||||
"comments": {"nodes": []},
|
"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 = AsyncMock()
|
||||||
mock_request.body.return_value = json.dumps(payload).encode()
|
mock_request.body.return_value = json.dumps(payload).encode()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue