open-swe/agent/webhooks/confluence_routes.py

82 lines
3.4 KiB
Python
Raw Permalink Normal View History

refactor: split webapp.py into api/ + per-source webhook routes 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.
2026-07-17 14:30:05 -04:00
"""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"}