mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
Plan step C4 (docs/upstream-sync/domain-reorg/reorg-build-plan.md, approved
decisions 1-2): split the 2,590-line agent/webapp.py monolith into
agent/webhooks/common.py (shared verify/dispatch helpers), agent/api/app.py
(composition), agent/api/health.py (/health + /webhooks/run-complete), and
per-source {github,linear,slack,jira,confluence}_routes.py. Atlassian
Connect lifecycle + descriptor routes (/connect/*) fold into
confluence_routes.py; webapp.py becomes the upstream-shaped compatibility
shim (from .api.app import app). langgraph.json http.app stays
agent.webapp:app via the shim.
Fork content, upstream layout: linear/slack route files verified
content-identical to upstream 8356eb34 and taken verbatim; github_routes is
upstream + the fork's CI auto-fix trigger wiring; jira/confluence routes are
fork-only, transformed to the same common.X / service.X module-attribute
style. All signature verification (GitHub HMAC, Slack, Linear
timestamp-freshness, verify_jira_secret + opt-in HMAC/timestamp/IP
allowlist, Connect JWT/qsh), token-attribution gating, TID-COLLIDE-01 repo
binding, _is_repo_auto_review_enabled gates, and public-repo org gate move
unchanged.
Handlers rewired from webapp.X to common.X; test monkeypatch sites across
26 files + conftest.py + e2e/harness.py retargeted to
webhook_common/handler/route modules per upstream's pattern. Residual
agent.webapp importers: only the shim, langgraph.json http.app, Makefile
uvicorn target, and docs (doc-path updates land in C7).
Gates: ruff check + format, pytest --co, full unit (1637 passed), full
Playwright E2E vs real langgraph dev (9/9), residual-importer sweep.
81 lines
3.4 KiB
Python
81 lines
3.4 KiB
Python
"""Confluence webhook HTTP routes (Atlassian Connect app).
|
|
|
|
The Confluence trigger is a private Atlassian Connect app, so the descriptor
|
|
and install/uninstall lifecycle callbacks (``/connect/*``) live here alongside
|
|
the JWT-verified ``comment_created`` webhook. JWT/qsh verification machinery
|
|
stays in ``agent.utils.atlassian_connect``.
|
|
"""
|
|
|
|
from fastapi import APIRouter
|
|
|
|
from . import common
|
|
from . import confluence as service
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/connect/atlassian-connect.json")
|
|
async def connect_descriptor() -> dict[str, common.Any]:
|
|
"""Serve the Atlassian Connect app descriptor (baseUrl from CONNECT_BASE_URL).
|
|
|
|
signed-install is true: Atlassian asymmetrically (RS256) signs the lifecycle
|
|
callbacks, so install/uninstall are cryptographically authenticated against
|
|
Atlassian's published keys (no trust-on-first-use). The comment_created
|
|
webhook stays symmetric (HS256 against the stored per-tenant sharedSecret).
|
|
"""
|
|
return {
|
|
"key": "sea-haven-open-swe-confluence",
|
|
"name": "Open SWE",
|
|
"description": "Triggers Open SWE runs from Confluence comments mentioning @openswe.",
|
|
"baseUrl": common.CONNECT_BASE_URL,
|
|
"vendor": {"name": "Sea Haven Industries", "url": "https://seahavenind.com"},
|
|
"authentication": {"type": "jwt"},
|
|
"apiMigrations": {"signed-install": True, "gdpr": True},
|
|
"lifecycle": {"installed": "/connect/installed", "uninstalled": "/connect/uninstalled"},
|
|
"scopes": ["READ"],
|
|
"modules": {
|
|
"webhooks": [{"event": "comment_created", "url": "/connect/webhook/comment-created"}]
|
|
},
|
|
}
|
|
|
|
|
|
@router.post("/connect/installed")
|
|
async def connect_installed(request: common.Request) -> common.Response:
|
|
"""Connect install lifecycle: trust-on-first-use (host-gated), verify re-install."""
|
|
try:
|
|
body = await request.json()
|
|
except Exception: # noqa: BLE001
|
|
raise common.HTTPException(status_code=400, detail="Invalid JSON") from None
|
|
code, detail = await service.process_install(request, body)
|
|
if code >= 400:
|
|
raise common.HTTPException(status_code=code, detail=detail)
|
|
return common.Response(status_code=code)
|
|
|
|
|
|
@router.post("/connect/uninstalled")
|
|
async def connect_uninstalled(request: common.Request) -> common.Response:
|
|
"""Connect uninstall lifecycle: verify against the stored secret before deleting."""
|
|
try:
|
|
body = await request.json()
|
|
except Exception: # noqa: BLE001
|
|
raise common.HTTPException(status_code=400, detail="Invalid JSON") from None
|
|
code, detail = await service.process_uninstall(request, body)
|
|
if code >= 400:
|
|
raise common.HTTPException(status_code=code, detail=detail)
|
|
return common.Response(status_code=code)
|
|
|
|
|
|
@router.post("/connect/webhook/comment-created")
|
|
async def connect_comment_created(
|
|
request: common.Request, background_tasks: common.BackgroundTasks
|
|
) -> dict[str, str]:
|
|
"""JWT-verified Confluence comment_created trigger."""
|
|
claims = await common.verify_connect_webhook(request)
|
|
if claims is None:
|
|
raise common.HTTPException(status_code=401, detail="Invalid Connect JWT")
|
|
try:
|
|
payload = await request.json()
|
|
except Exception: # noqa: BLE001
|
|
return {"status": "error", "message": "Invalid JSON"}
|
|
background_tasks.add_task(service.process_confluence_comment, payload, claims.get("iss", ""))
|
|
return {"status": "accepted"}
|