open-swe/agent/completion.py

257 lines
11 KiB
Python
Raw Normal View History

refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
"""Run-completion webhook handler — guarantees every run ends with a signal.
The platform POSTs a run-completion payload to ``/webhooks/run-complete`` (wired
as the ``webhook`` on every dispatched run, see ``agent.dispatch``). When a run
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
ends in a failure state (``error`` / ``timeout``) we post a
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
short failure reply to the originating channel, so a run that died on a server
recycle or hit a limit never leaves the user in silence.
This decouples "the user gets an answer" from "the agent remembered to reply."
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
The reply is idempotent per run: the run id (or, for manual payloads without
one, a run-distinguishing ``updated_at``/``created_at`` marker) is claimed in a
bounded per-thread list *before* the post (claim-then-post), so a retried or
concurrent completion webhook can't double-post. A payload that carries nothing
to distinguish one run from another posts without deduping — a rare duplicate is
preferred over silencing a later, genuinely different failed run.
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
"""
from __future__ import annotations
import hmac
import logging
import os
from collections.abc import Awaitable, Callable
from typing import Any
feat: Jira + Confluence integration (tools + triggers) (#182) * feat(open-swe): add Jira tool plane (Phase 1) Curated Jira Cloud REST v3 toolset for the agent, mirroring the Linear tools: - utils/jira.py: service-account REST client (Basic auth) with get/ create/update issue, comments, list projects, trace comment; issue and comment bodies normalized to markdown. - utils/adf.py: minimal ADF <-> markdown conversion (read paths convert Jira ADF to markdown; agent comments convert prose to ADF). - tools/jira_{comment,get_issue,get_issue_comments,create_issue, update_issue,list_projects}.py wired into the tool registry and the main agent tool list. - tests/test_jira_utils.py: ADF conversion + mocked-transport util tests. Reads JIRA_BASE_URL / JIRA_SERVICE_EMAIL / JIRA_API_TOKEN; unset env returns a clean error, so this is safe to land dark. Trigger plane, prompt guidance, and config plumbing follow in Phase 2. * feat(open-swe): add Confluence tool plane (Phase 3) Curated Confluence Cloud REST toolset for the agent, mirroring the Jira tools: - utils/confluence.py: service-account REST client (Basic auth) with get/create/update page, add comment, CQL search. Page bodies are XHTML storage format (not ADF), with minimal storage<->text converters; update_page reads the current version and bumps it, as Confluence requires. - tools/confluence_{get_page,create_page,update_page,comment,search}.py registered in the tool registry. - tests/test_confluence_utils.py: converter + mocked-transport tests including the version-bump path. Reads CONFLUENCE_BASE_URL / CONFLUENCE_EMAIL / CONFLUENCE_API_TOKEN; unset env returns a clean error. Activation in the agent tool list lands with the Phase 2 server.py wiring. * feat(open-swe): add Jira trigger plane (Phase 2) Make an @openswe comment on a Jira issue spawn an agent run, mirroring the Linear trigger plane: - webhooks/jira.py: process_jira_issue clones process_linear_issue — deterministic thread id, full-issue fetch, actor accountId->email attribution feeding resolve_login_from_email_async (PRs open as the human), multimodal image handling, source="jira" + jira_issue config. - webapp.py: POST/GET /webhooks/jira, verify_jira_secret (constant-time X-Automation-Webhook-Token check, fails closed), repo-resolution cascade, get_repo_config_from_jira_mapping. - utils/jira_project_repo_map.py: JIRA_PROJECT_TO_REPO (placeholder entry — real project->repo mappings still needed). - utils/jira.py: get_user_email (accountId -> email) for attribution. - completion.py: source=="jira" failure-reply branch. - prompt.py: Jira-triggered notify guidance + Refs:/branch key from {jira_project_key}-{jira_issue_number}. - server.py: read jira_issue config + pass jira key to the system prompt; also activates the Phase 3 Confluence tools in the agent list. Jira Automation lacks native webhook HMAC signing, so trust is a shared secret header (decision D2); replay protection is weaker than Linear's HMAC+timestamp. /sh-security-review + an Atlassian IP allowlist are the outstanding gate/hardening before push. * fix(open-swe): harden Jira webhook trust (sh-security-review) Resolves findings from the Phase 2 security review (detector fan-out + proof-or-kill verifier). The unsigned Jira Automation webhook body was trusted for identity, comment content, repo routing, and issue existence; a JIRA_WEBHOOK_SECRET holder could forge those fields. - Corroborate against the real Jira record: the webhook body is now only a pointer (issue_key + required comment_id). The triggering comment's author and text are re-fetched server-side via get_comment/fetch_jira_ comment, and identity, the @openswe check, prompt text, and project key are derived from that authoritative record — never payload author/ body fields. An uncorroborated comment is rejected. (closes the account-id impersonation, unsigned-body prompt injection, and fabricated-issue findings) - Validate issue_key against the Jira key format and percent-encode all untrusted path segments (_seg) so a crafted key can't traverse to a different Jira REST endpoint or inject query params. (closes the path- traversal / query-injection findings) - Route source=="jira" through the bot-token-default / author_prs_as_ user opt-in path in resolve_github_token, matching Linear, instead of unconditionally resolving a per-user OAuth token from a payload email. - Gate attribution on an active user mapping (is_login_mapped) so a pending/unconfirmed mapping can't drive PR authorship. Adds regression tests: server-corroboration wins over payload, malformed issue_key rejected, uncorroborated comment rejected, path-segment encoding, project-key derivation, active-mapping gate. Remaining (non-blocking, deployment/hardening): set ALLOWED_GITHUB_ORGS/ REPOS so the shared allowlist isn't fail-open; consider HMAC-over-body + timestamp on the Automation payload to close the residual replay gap. * harden(open-swe): opt-in Jira webhook replay/IP + fail-closed allowlist Folds the two deployment-hardening items from the Phase 2 security review into code (all opt-in / default-off, so existing and upstream deployments are unaffected): - JIRA_WEBHOOK_REQUIRE_SIGNATURE: when set, the Automation payload must carry X-Openswe-Signature (hex HMAC-SHA256 of the raw body keyed by JIRA_WEBHOOK_SECRET) plus a fresh timestamp, verified by verify_jira_signature / _jira_timestamp_is_fresh (mirrors the Linear HMAC+freshness model). Closes the static-token model's replay/forgery gap when enabled. - JIRA_WEBHOOK_IP_ALLOWLIST: optional CIDR allowlist on the webhook's direct client IP (verify_jira_source_ip). Documented as direct-peer only; behind a proxy/LB, allowlist Atlassian's ranges at that layer. - REQUIRE_REPO_ALLOWLIST: makes an empty ALLOWED_GITHUB_ORGS/REPOS fail CLOSED instead of the back-compat allow-all, plus a startup fail-open warning. Applies to all channels for consistency. Documents all new vars (and a Jira section) in .env.example. Adds tests for signature on/off + valid/missing/wrong/stale, IP allow/deny/off, and the fail-closed allowlist. * feat(open-swe): Confluence Atlassian Connect trigger (Phase 4) Adds the @openswe-on-a-Confluence-comment trigger via a private Atlassian Connect app. Designed and adversarially verified with the ultracode workflow (3 divergent Opus designs + judge; 3 proof-or-kill Opus skeptics on the implemented crypto). - utils/atlassian_connect.py: hand-rolled qsh (pinned to Atlassian's official test vector), PyJWT HS256 webhook verifier with alg-pinning, issuer binding, and qsh-verified-last ordering; RS256 signed-install lifecycle verifier against Atlassian's published keys; installation store keyed by clientKey with the sharedSecret encrypted at rest (TOKEN_ENCRYPTION_KEY / Fernet). No new dependency (PyJWT already pinned). - webhooks/confluence.py: install/uninstall lifecycle + comment handler. The JWT-signed webhook body is only a pointer; the comment's real author/text/container are re-fetched server-side via the Basic-auth service account (Phase-2 corroboration lesson), with active-only login attribution and the repo allowlist. - utils/confluence.py: get_comment / get_user_email (path-encoded). - webapp.py: GET /connect/atlassian-connect.json (served dynamically), POST /connect/{installed,uninstalled,webhook/comment-created}, the space->repo resolver, thread-id, and fetch helpers. - completion.py: source=="confluence" failure-reply branch. Security: the sh-security-review verify pass confirmed one HIGH — the symmetric signed-install=false first-install was trust-on-first-use gated only by the public Confluence hostname (webhook-auth bypass). Fixed by switching to signed-install=true + RS256 verification of lifecycle callbacks, which cryptographically authenticates the first install. All other attack lenses (forgery/replay/alg-confusion/overwrite/uninstall DoS/corroboration/injection) were defeated; residuals are deployment config (REQUIRE_REPO_ALLOWLIST) or accepted-by-design (qsh cannot cover bodies; comment-trigger prompt injection, shared with all sources). New env (documented in .env.example): CONFLUENCE_BASE_URL/EMAIL/API_TOKEN, CONNECT_BASE_URL, CONNECT_EXPECTED_BASE_URL (optional). Install secrets require the durable Postgres LangGraph store in prod. Outstanding before push: /sh-security-review on the real diff and the GPT-4.1 cross-family review (auth boundary); README/CLAUDE.md + memory. * docs(open-swe): Phase 5 — Confluence prompt guidance + architecture docs - prompt.py: Confluence-triggered runs notify via confluence_comment on the triggering page; add Confluence to the shared-base source list. - CLAUDE.md: document the Jira + Confluence tool planes and the Atlassian triggers (Jira Automation shared-secret webhook; Confluence Connect app with HS256 webhook + qsh and RS256 signed-install lifecycle), plus the server-side corroboration + encrypted install store. Phase 5 also verified the trigger surface end-to-end against a running uvicorn app (descriptor served; /connect/* and /webhooks/jira fail closed without valid auth) and recorded the integration in project memory. * fix(open-swe): resolve /sh-security-review findings on the Atlassian surface Formal sh-security-review (detector fan-out + verifier) over the Phase-4 Connect surface (esp. the new RS256 signed-install code, unseen by the earlier adversarial verify) and the Phase-2 opt-in hardening. CRITICAL — cross-tenant install (origin validation, CWE-346): signed- install proves the caller is *an* Atlassian tenant, not *ours*, and the descriptor is served publicly, so any attacker could install the app on their own Confluence site and drive agent runs against our allowlisted repos. The baseUrl body field is attacker-controlled and cannot bind the tenant; only the signature-verified clientKey (JWT iss) can. Added a MANDATORY, fail-closed CONNECT_EXPECTED_CLIENT_KEYS allowlist checked in process_install after signature+iss verification. HIGH — cross-tenant thread-id collision (CWE-330/863): Confluence comment ids are per-instance, so generate_thread_id_from_confluence_comment now salts the hash with the verified clientKey (plumbed from the webhook JWT iss) to prevent thread hijack across tenants. HIGH/MEDIUM — path/query injection (CWE-22/88): get_page and update_page interpolated page_id into the REST path unencoded (update_page on a mutating PUT with no params= backstop). Now _seg()-encoded, matching the rest of the module. MEDIUM — self-trigger loop (CWE-405): process_confluence_comment had no bot-authorship early-out. Added an optional CONFLUENCE_BOT_ACCOUNT_ID guard mirroring the Linear botActor / Jira comment_author_is_bot checks. LOW — corrected the CONNECT_EXPECTED_BASE_URL comment to document it as opt-in defense-in-depth (the clientKey allowlist is the real gate). Verified clean by the detectors: RS256/HS256 alg-pinning, aud/iss/exp, kid-fetch SSRF (host-pinned + quote-encoded), at-rest secret encryption, constant-time comparisons, and the Phase-2 hardening. New regression tests for each fix; full suite green (1602). * harden(open-swe): GPT-4.1 cross-family review follow-ups Cross-family review (GPT-4.1 via orchestrator cross_reviewer) found no critical/high issues and confirmed the auth boundary is fail-closed and correct. Two low-cost defense-in-depth items applied: - Validate the signed-install JWT 'kid' against a strict charset before the public-key fetch, so a malformed kid fails fast with no network call (on top of the existing fixed host + percent-encoding). - Make JWT nbf verification explicit (verify_nbf) on both the RS256 lifecycle and HS256 webhook decodes. Other suggestions triaged as already-handled (aud cross-app replay is blocked by the per-tenant iss->secret lookup; documented static-token/IP/ baseUrl tradeoffs; qsh pinned to Atlassian's vector) or ops/infra (Fernet rotation via MultiFernet; rate limiting at the gateway). * docs(open-swe): document Jira + Confluence in installation & customization guides - INSTALLATION.md §5: add Jira (Automation-rule webhook + shared secret, service account, JIRA_PROJECT_TO_REPO) and Confluence (Atlassian Connect app install, CONNECT_EXPECTED_CLIENT_KEYS bootstrap, durable-store note, CONFLUENCE_SPACE_TO_REPO) trigger setup; §6: add the new env vars + REQUIRE_REPO_ALLOWLIST. - CUSTOMIZATION.md: jira_*/confluence_* in the tools table; repo-extraction note covers all four sources. - AGENTS.md: match CLAUDE.md (triggers, webhooks, tool list, auth). - README.md: invocation section, tools table, and overview line.
2026-07-13 19:45:54 -04:00
from .utils.confluence import add_comment as add_confluence_comment
from .utils.dashboard_links import dashboard_thread_url
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
from .utils.github_app import get_github_app_installation_token
from .utils.github_comments import post_github_comment
feat: Jira + Confluence integration (tools + triggers) (#182) * feat(open-swe): add Jira tool plane (Phase 1) Curated Jira Cloud REST v3 toolset for the agent, mirroring the Linear tools: - utils/jira.py: service-account REST client (Basic auth) with get/ create/update issue, comments, list projects, trace comment; issue and comment bodies normalized to markdown. - utils/adf.py: minimal ADF <-> markdown conversion (read paths convert Jira ADF to markdown; agent comments convert prose to ADF). - tools/jira_{comment,get_issue,get_issue_comments,create_issue, update_issue,list_projects}.py wired into the tool registry and the main agent tool list. - tests/test_jira_utils.py: ADF conversion + mocked-transport util tests. Reads JIRA_BASE_URL / JIRA_SERVICE_EMAIL / JIRA_API_TOKEN; unset env returns a clean error, so this is safe to land dark. Trigger plane, prompt guidance, and config plumbing follow in Phase 2. * feat(open-swe): add Confluence tool plane (Phase 3) Curated Confluence Cloud REST toolset for the agent, mirroring the Jira tools: - utils/confluence.py: service-account REST client (Basic auth) with get/create/update page, add comment, CQL search. Page bodies are XHTML storage format (not ADF), with minimal storage<->text converters; update_page reads the current version and bumps it, as Confluence requires. - tools/confluence_{get_page,create_page,update_page,comment,search}.py registered in the tool registry. - tests/test_confluence_utils.py: converter + mocked-transport tests including the version-bump path. Reads CONFLUENCE_BASE_URL / CONFLUENCE_EMAIL / CONFLUENCE_API_TOKEN; unset env returns a clean error. Activation in the agent tool list lands with the Phase 2 server.py wiring. * feat(open-swe): add Jira trigger plane (Phase 2) Make an @openswe comment on a Jira issue spawn an agent run, mirroring the Linear trigger plane: - webhooks/jira.py: process_jira_issue clones process_linear_issue — deterministic thread id, full-issue fetch, actor accountId->email attribution feeding resolve_login_from_email_async (PRs open as the human), multimodal image handling, source="jira" + jira_issue config. - webapp.py: POST/GET /webhooks/jira, verify_jira_secret (constant-time X-Automation-Webhook-Token check, fails closed), repo-resolution cascade, get_repo_config_from_jira_mapping. - utils/jira_project_repo_map.py: JIRA_PROJECT_TO_REPO (placeholder entry — real project->repo mappings still needed). - utils/jira.py: get_user_email (accountId -> email) for attribution. - completion.py: source=="jira" failure-reply branch. - prompt.py: Jira-triggered notify guidance + Refs:/branch key from {jira_project_key}-{jira_issue_number}. - server.py: read jira_issue config + pass jira key to the system prompt; also activates the Phase 3 Confluence tools in the agent list. Jira Automation lacks native webhook HMAC signing, so trust is a shared secret header (decision D2); replay protection is weaker than Linear's HMAC+timestamp. /sh-security-review + an Atlassian IP allowlist are the outstanding gate/hardening before push. * fix(open-swe): harden Jira webhook trust (sh-security-review) Resolves findings from the Phase 2 security review (detector fan-out + proof-or-kill verifier). The unsigned Jira Automation webhook body was trusted for identity, comment content, repo routing, and issue existence; a JIRA_WEBHOOK_SECRET holder could forge those fields. - Corroborate against the real Jira record: the webhook body is now only a pointer (issue_key + required comment_id). The triggering comment's author and text are re-fetched server-side via get_comment/fetch_jira_ comment, and identity, the @openswe check, prompt text, and project key are derived from that authoritative record — never payload author/ body fields. An uncorroborated comment is rejected. (closes the account-id impersonation, unsigned-body prompt injection, and fabricated-issue findings) - Validate issue_key against the Jira key format and percent-encode all untrusted path segments (_seg) so a crafted key can't traverse to a different Jira REST endpoint or inject query params. (closes the path- traversal / query-injection findings) - Route source=="jira" through the bot-token-default / author_prs_as_ user opt-in path in resolve_github_token, matching Linear, instead of unconditionally resolving a per-user OAuth token from a payload email. - Gate attribution on an active user mapping (is_login_mapped) so a pending/unconfirmed mapping can't drive PR authorship. Adds regression tests: server-corroboration wins over payload, malformed issue_key rejected, uncorroborated comment rejected, path-segment encoding, project-key derivation, active-mapping gate. Remaining (non-blocking, deployment/hardening): set ALLOWED_GITHUB_ORGS/ REPOS so the shared allowlist isn't fail-open; consider HMAC-over-body + timestamp on the Automation payload to close the residual replay gap. * harden(open-swe): opt-in Jira webhook replay/IP + fail-closed allowlist Folds the two deployment-hardening items from the Phase 2 security review into code (all opt-in / default-off, so existing and upstream deployments are unaffected): - JIRA_WEBHOOK_REQUIRE_SIGNATURE: when set, the Automation payload must carry X-Openswe-Signature (hex HMAC-SHA256 of the raw body keyed by JIRA_WEBHOOK_SECRET) plus a fresh timestamp, verified by verify_jira_signature / _jira_timestamp_is_fresh (mirrors the Linear HMAC+freshness model). Closes the static-token model's replay/forgery gap when enabled. - JIRA_WEBHOOK_IP_ALLOWLIST: optional CIDR allowlist on the webhook's direct client IP (verify_jira_source_ip). Documented as direct-peer only; behind a proxy/LB, allowlist Atlassian's ranges at that layer. - REQUIRE_REPO_ALLOWLIST: makes an empty ALLOWED_GITHUB_ORGS/REPOS fail CLOSED instead of the back-compat allow-all, plus a startup fail-open warning. Applies to all channels for consistency. Documents all new vars (and a Jira section) in .env.example. Adds tests for signature on/off + valid/missing/wrong/stale, IP allow/deny/off, and the fail-closed allowlist. * feat(open-swe): Confluence Atlassian Connect trigger (Phase 4) Adds the @openswe-on-a-Confluence-comment trigger via a private Atlassian Connect app. Designed and adversarially verified with the ultracode workflow (3 divergent Opus designs + judge; 3 proof-or-kill Opus skeptics on the implemented crypto). - utils/atlassian_connect.py: hand-rolled qsh (pinned to Atlassian's official test vector), PyJWT HS256 webhook verifier with alg-pinning, issuer binding, and qsh-verified-last ordering; RS256 signed-install lifecycle verifier against Atlassian's published keys; installation store keyed by clientKey with the sharedSecret encrypted at rest (TOKEN_ENCRYPTION_KEY / Fernet). No new dependency (PyJWT already pinned). - webhooks/confluence.py: install/uninstall lifecycle + comment handler. The JWT-signed webhook body is only a pointer; the comment's real author/text/container are re-fetched server-side via the Basic-auth service account (Phase-2 corroboration lesson), with active-only login attribution and the repo allowlist. - utils/confluence.py: get_comment / get_user_email (path-encoded). - webapp.py: GET /connect/atlassian-connect.json (served dynamically), POST /connect/{installed,uninstalled,webhook/comment-created}, the space->repo resolver, thread-id, and fetch helpers. - completion.py: source=="confluence" failure-reply branch. Security: the sh-security-review verify pass confirmed one HIGH — the symmetric signed-install=false first-install was trust-on-first-use gated only by the public Confluence hostname (webhook-auth bypass). Fixed by switching to signed-install=true + RS256 verification of lifecycle callbacks, which cryptographically authenticates the first install. All other attack lenses (forgery/replay/alg-confusion/overwrite/uninstall DoS/corroboration/injection) were defeated; residuals are deployment config (REQUIRE_REPO_ALLOWLIST) or accepted-by-design (qsh cannot cover bodies; comment-trigger prompt injection, shared with all sources). New env (documented in .env.example): CONFLUENCE_BASE_URL/EMAIL/API_TOKEN, CONNECT_BASE_URL, CONNECT_EXPECTED_BASE_URL (optional). Install secrets require the durable Postgres LangGraph store in prod. Outstanding before push: /sh-security-review on the real diff and the GPT-4.1 cross-family review (auth boundary); README/CLAUDE.md + memory. * docs(open-swe): Phase 5 — Confluence prompt guidance + architecture docs - prompt.py: Confluence-triggered runs notify via confluence_comment on the triggering page; add Confluence to the shared-base source list. - CLAUDE.md: document the Jira + Confluence tool planes and the Atlassian triggers (Jira Automation shared-secret webhook; Confluence Connect app with HS256 webhook + qsh and RS256 signed-install lifecycle), plus the server-side corroboration + encrypted install store. Phase 5 also verified the trigger surface end-to-end against a running uvicorn app (descriptor served; /connect/* and /webhooks/jira fail closed without valid auth) and recorded the integration in project memory. * fix(open-swe): resolve /sh-security-review findings on the Atlassian surface Formal sh-security-review (detector fan-out + verifier) over the Phase-4 Connect surface (esp. the new RS256 signed-install code, unseen by the earlier adversarial verify) and the Phase-2 opt-in hardening. CRITICAL — cross-tenant install (origin validation, CWE-346): signed- install proves the caller is *an* Atlassian tenant, not *ours*, and the descriptor is served publicly, so any attacker could install the app on their own Confluence site and drive agent runs against our allowlisted repos. The baseUrl body field is attacker-controlled and cannot bind the tenant; only the signature-verified clientKey (JWT iss) can. Added a MANDATORY, fail-closed CONNECT_EXPECTED_CLIENT_KEYS allowlist checked in process_install after signature+iss verification. HIGH — cross-tenant thread-id collision (CWE-330/863): Confluence comment ids are per-instance, so generate_thread_id_from_confluence_comment now salts the hash with the verified clientKey (plumbed from the webhook JWT iss) to prevent thread hijack across tenants. HIGH/MEDIUM — path/query injection (CWE-22/88): get_page and update_page interpolated page_id into the REST path unencoded (update_page on a mutating PUT with no params= backstop). Now _seg()-encoded, matching the rest of the module. MEDIUM — self-trigger loop (CWE-405): process_confluence_comment had no bot-authorship early-out. Added an optional CONFLUENCE_BOT_ACCOUNT_ID guard mirroring the Linear botActor / Jira comment_author_is_bot checks. LOW — corrected the CONNECT_EXPECTED_BASE_URL comment to document it as opt-in defense-in-depth (the clientKey allowlist is the real gate). Verified clean by the detectors: RS256/HS256 alg-pinning, aud/iss/exp, kid-fetch SSRF (host-pinned + quote-encoded), at-rest secret encryption, constant-time comparisons, and the Phase-2 hardening. New regression tests for each fix; full suite green (1602). * harden(open-swe): GPT-4.1 cross-family review follow-ups Cross-family review (GPT-4.1 via orchestrator cross_reviewer) found no critical/high issues and confirmed the auth boundary is fail-closed and correct. Two low-cost defense-in-depth items applied: - Validate the signed-install JWT 'kid' against a strict charset before the public-key fetch, so a malformed kid fails fast with no network call (on top of the existing fixed host + percent-encoding). - Make JWT nbf verification explicit (verify_nbf) on both the RS256 lifecycle and HS256 webhook decodes. Other suggestions triaged as already-handled (aud cross-app replay is blocked by the per-tenant iss->secret lookup; documented static-token/IP/ baseUrl tradeoffs; qsh pinned to Atlassian's vector) or ops/infra (Fernet rotation via MultiFernet; rate limiting at the gateway). * docs(open-swe): document Jira + Confluence in installation & customization guides - INSTALLATION.md §5: add Jira (Automation-rule webhook + shared secret, service account, JIRA_PROJECT_TO_REPO) and Confluence (Atlassian Connect app install, CONNECT_EXPECTED_CLIENT_KEYS bootstrap, durable-store note, CONFLUENCE_SPACE_TO_REPO) trigger setup; §6: add the new env vars + REQUIRE_REPO_ALLOWLIST. - CUSTOMIZATION.md: jira_*/confluence_* in the tools table; repo-extraction note covers all four sources. - AGENTS.md: match CLAUDE.md (triggers, webhooks, tool list, auth). - README.md: invocation section, tools table, and overview line.
2026-07-13 19:45:54 -04:00
from .utils.jira import comment_on_issue as comment_on_jira_issue
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
from .utils.linear import comment_on_linear_issue
from .utils.slack import post_slack_thread_reply
from .utils.thread_ops import langgraph_client
logger = logging.getLogger(__name__)
# Run statuses that mean the user will otherwise get nothing back. "interrupted"
# is intentionally excluded: with multitask_strategy="interrupt", a normal
# follow-up halts the prior run (status "interrupted") while its replacement
# carries on — that's healthy, not a failure worth a "couldn't finish" reply.
_TERMINAL_FAILURE_STATUSES = frozenset({"error", "timeout"})
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
_FAILURE_REPLY_RUN_ID = "failure_reply_posted_run_id"
_FAILURE_REPLY_RUN_IDS = "failure_reply_posted_run_ids"
_MAX_FAILURE_REPLY_RUN_IDS = 20
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
class _ClaimFailed(Exception):
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
"""Raised when the dedup key couldn't be claimed, so we skip the post."""
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
# Shared-secret bearer token proving a /webhooks/run-complete call came from our
# own dispatch (which appends ?token= when this is set) rather than from an
# attacker hitting the public route. Fail closed when unset: the route rejects
# every call, so completion replies stay off until the secret is configured.
RUN_COMPLETE_WEBHOOK_SECRET = os.environ.get("RUN_COMPLETE_WEBHOOK_SECRET")
if not RUN_COMPLETE_WEBHOOK_SECRET:
logger.warning(
"RUN_COMPLETE_WEBHOOK_SECRET is not set; /webhooks/run-complete is fail-closed "
"(all calls rejected) and run-failure replies are disabled. Set it to enable them."
)
def verify_run_complete_token(token: str | None) -> bool:
"""Return whether a run-completion webhook token is acceptable.
Fail closed: with no secret configured, reject every call rather than accept
unauthenticated requests on a publicly reachable route.
"""
secret = RUN_COMPLETE_WEBHOOK_SECRET
if not secret:
return False
return token is not None and hmac.compare_digest(token, secret)
def _failure_text(status: str, dashboard_url: str | None = None) -> str:
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
if status == "timeout":
reason = "timed out"
elif status == "interrupted":
reason = "was interrupted before it could finish"
else:
reason = "hit an unexpected error"
text = (
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
f"\u26a0\ufe0f I wasn't able to finish that \u2014 the run {reason}. "
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
"Send another message and I'll pick it back up."
)
if dashboard_url:
text += f" You can view the error in <{dashboard_url}|Open SWE Web>."
return text
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
async def _post_failure_reply(
thread_id: str,
metadata: dict[str, Any],
status: str,
*,
claim: Callable[[], Awaitable[None]],
) -> bool:
"""Post a failure reply to the run's originating channel. Best-effort.
``claim`` is awaited immediately before the network post (claim-then-post),
only on a branch that actually delivers, so a retried/concurrent webhook
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
can't double-post and threads with no reply channel never burn the claim.
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
"""
source = metadata.get("source")
ctx = metadata.get("source_context")
ctx = ctx if isinstance(ctx, dict) else {}
text = _failure_text(status)
if source == "slack":
slack_thread = ctx.get("slack_thread")
if isinstance(slack_thread, dict):
channel_id = slack_thread.get("channel_id")
thread_ts = slack_thread.get("thread_ts")
if channel_id and thread_ts:
await claim()
slack_text = _failure_text(status, dashboard_thread_url(thread_id))
return await post_slack_thread_reply(channel_id, thread_ts, slack_text)
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
return False
if source == "linear":
linear_issue = ctx.get("linear_issue")
if isinstance(linear_issue, dict):
issue_id = linear_issue.get("id")
if issue_id:
await claim()
return await comment_on_linear_issue(issue_id, text)
return False
feat: Jira + Confluence integration (tools + triggers) (#182) * feat(open-swe): add Jira tool plane (Phase 1) Curated Jira Cloud REST v3 toolset for the agent, mirroring the Linear tools: - utils/jira.py: service-account REST client (Basic auth) with get/ create/update issue, comments, list projects, trace comment; issue and comment bodies normalized to markdown. - utils/adf.py: minimal ADF <-> markdown conversion (read paths convert Jira ADF to markdown; agent comments convert prose to ADF). - tools/jira_{comment,get_issue,get_issue_comments,create_issue, update_issue,list_projects}.py wired into the tool registry and the main agent tool list. - tests/test_jira_utils.py: ADF conversion + mocked-transport util tests. Reads JIRA_BASE_URL / JIRA_SERVICE_EMAIL / JIRA_API_TOKEN; unset env returns a clean error, so this is safe to land dark. Trigger plane, prompt guidance, and config plumbing follow in Phase 2. * feat(open-swe): add Confluence tool plane (Phase 3) Curated Confluence Cloud REST toolset for the agent, mirroring the Jira tools: - utils/confluence.py: service-account REST client (Basic auth) with get/create/update page, add comment, CQL search. Page bodies are XHTML storage format (not ADF), with minimal storage<->text converters; update_page reads the current version and bumps it, as Confluence requires. - tools/confluence_{get_page,create_page,update_page,comment,search}.py registered in the tool registry. - tests/test_confluence_utils.py: converter + mocked-transport tests including the version-bump path. Reads CONFLUENCE_BASE_URL / CONFLUENCE_EMAIL / CONFLUENCE_API_TOKEN; unset env returns a clean error. Activation in the agent tool list lands with the Phase 2 server.py wiring. * feat(open-swe): add Jira trigger plane (Phase 2) Make an @openswe comment on a Jira issue spawn an agent run, mirroring the Linear trigger plane: - webhooks/jira.py: process_jira_issue clones process_linear_issue — deterministic thread id, full-issue fetch, actor accountId->email attribution feeding resolve_login_from_email_async (PRs open as the human), multimodal image handling, source="jira" + jira_issue config. - webapp.py: POST/GET /webhooks/jira, verify_jira_secret (constant-time X-Automation-Webhook-Token check, fails closed), repo-resolution cascade, get_repo_config_from_jira_mapping. - utils/jira_project_repo_map.py: JIRA_PROJECT_TO_REPO (placeholder entry — real project->repo mappings still needed). - utils/jira.py: get_user_email (accountId -> email) for attribution. - completion.py: source=="jira" failure-reply branch. - prompt.py: Jira-triggered notify guidance + Refs:/branch key from {jira_project_key}-{jira_issue_number}. - server.py: read jira_issue config + pass jira key to the system prompt; also activates the Phase 3 Confluence tools in the agent list. Jira Automation lacks native webhook HMAC signing, so trust is a shared secret header (decision D2); replay protection is weaker than Linear's HMAC+timestamp. /sh-security-review + an Atlassian IP allowlist are the outstanding gate/hardening before push. * fix(open-swe): harden Jira webhook trust (sh-security-review) Resolves findings from the Phase 2 security review (detector fan-out + proof-or-kill verifier). The unsigned Jira Automation webhook body was trusted for identity, comment content, repo routing, and issue existence; a JIRA_WEBHOOK_SECRET holder could forge those fields. - Corroborate against the real Jira record: the webhook body is now only a pointer (issue_key + required comment_id). The triggering comment's author and text are re-fetched server-side via get_comment/fetch_jira_ comment, and identity, the @openswe check, prompt text, and project key are derived from that authoritative record — never payload author/ body fields. An uncorroborated comment is rejected. (closes the account-id impersonation, unsigned-body prompt injection, and fabricated-issue findings) - Validate issue_key against the Jira key format and percent-encode all untrusted path segments (_seg) so a crafted key can't traverse to a different Jira REST endpoint or inject query params. (closes the path- traversal / query-injection findings) - Route source=="jira" through the bot-token-default / author_prs_as_ user opt-in path in resolve_github_token, matching Linear, instead of unconditionally resolving a per-user OAuth token from a payload email. - Gate attribution on an active user mapping (is_login_mapped) so a pending/unconfirmed mapping can't drive PR authorship. Adds regression tests: server-corroboration wins over payload, malformed issue_key rejected, uncorroborated comment rejected, path-segment encoding, project-key derivation, active-mapping gate. Remaining (non-blocking, deployment/hardening): set ALLOWED_GITHUB_ORGS/ REPOS so the shared allowlist isn't fail-open; consider HMAC-over-body + timestamp on the Automation payload to close the residual replay gap. * harden(open-swe): opt-in Jira webhook replay/IP + fail-closed allowlist Folds the two deployment-hardening items from the Phase 2 security review into code (all opt-in / default-off, so existing and upstream deployments are unaffected): - JIRA_WEBHOOK_REQUIRE_SIGNATURE: when set, the Automation payload must carry X-Openswe-Signature (hex HMAC-SHA256 of the raw body keyed by JIRA_WEBHOOK_SECRET) plus a fresh timestamp, verified by verify_jira_signature / _jira_timestamp_is_fresh (mirrors the Linear HMAC+freshness model). Closes the static-token model's replay/forgery gap when enabled. - JIRA_WEBHOOK_IP_ALLOWLIST: optional CIDR allowlist on the webhook's direct client IP (verify_jira_source_ip). Documented as direct-peer only; behind a proxy/LB, allowlist Atlassian's ranges at that layer. - REQUIRE_REPO_ALLOWLIST: makes an empty ALLOWED_GITHUB_ORGS/REPOS fail CLOSED instead of the back-compat allow-all, plus a startup fail-open warning. Applies to all channels for consistency. Documents all new vars (and a Jira section) in .env.example. Adds tests for signature on/off + valid/missing/wrong/stale, IP allow/deny/off, and the fail-closed allowlist. * feat(open-swe): Confluence Atlassian Connect trigger (Phase 4) Adds the @openswe-on-a-Confluence-comment trigger via a private Atlassian Connect app. Designed and adversarially verified with the ultracode workflow (3 divergent Opus designs + judge; 3 proof-or-kill Opus skeptics on the implemented crypto). - utils/atlassian_connect.py: hand-rolled qsh (pinned to Atlassian's official test vector), PyJWT HS256 webhook verifier with alg-pinning, issuer binding, and qsh-verified-last ordering; RS256 signed-install lifecycle verifier against Atlassian's published keys; installation store keyed by clientKey with the sharedSecret encrypted at rest (TOKEN_ENCRYPTION_KEY / Fernet). No new dependency (PyJWT already pinned). - webhooks/confluence.py: install/uninstall lifecycle + comment handler. The JWT-signed webhook body is only a pointer; the comment's real author/text/container are re-fetched server-side via the Basic-auth service account (Phase-2 corroboration lesson), with active-only login attribution and the repo allowlist. - utils/confluence.py: get_comment / get_user_email (path-encoded). - webapp.py: GET /connect/atlassian-connect.json (served dynamically), POST /connect/{installed,uninstalled,webhook/comment-created}, the space->repo resolver, thread-id, and fetch helpers. - completion.py: source=="confluence" failure-reply branch. Security: the sh-security-review verify pass confirmed one HIGH — the symmetric signed-install=false first-install was trust-on-first-use gated only by the public Confluence hostname (webhook-auth bypass). Fixed by switching to signed-install=true + RS256 verification of lifecycle callbacks, which cryptographically authenticates the first install. All other attack lenses (forgery/replay/alg-confusion/overwrite/uninstall DoS/corroboration/injection) were defeated; residuals are deployment config (REQUIRE_REPO_ALLOWLIST) or accepted-by-design (qsh cannot cover bodies; comment-trigger prompt injection, shared with all sources). New env (documented in .env.example): CONFLUENCE_BASE_URL/EMAIL/API_TOKEN, CONNECT_BASE_URL, CONNECT_EXPECTED_BASE_URL (optional). Install secrets require the durable Postgres LangGraph store in prod. Outstanding before push: /sh-security-review on the real diff and the GPT-4.1 cross-family review (auth boundary); README/CLAUDE.md + memory. * docs(open-swe): Phase 5 — Confluence prompt guidance + architecture docs - prompt.py: Confluence-triggered runs notify via confluence_comment on the triggering page; add Confluence to the shared-base source list. - CLAUDE.md: document the Jira + Confluence tool planes and the Atlassian triggers (Jira Automation shared-secret webhook; Confluence Connect app with HS256 webhook + qsh and RS256 signed-install lifecycle), plus the server-side corroboration + encrypted install store. Phase 5 also verified the trigger surface end-to-end against a running uvicorn app (descriptor served; /connect/* and /webhooks/jira fail closed without valid auth) and recorded the integration in project memory. * fix(open-swe): resolve /sh-security-review findings on the Atlassian surface Formal sh-security-review (detector fan-out + verifier) over the Phase-4 Connect surface (esp. the new RS256 signed-install code, unseen by the earlier adversarial verify) and the Phase-2 opt-in hardening. CRITICAL — cross-tenant install (origin validation, CWE-346): signed- install proves the caller is *an* Atlassian tenant, not *ours*, and the descriptor is served publicly, so any attacker could install the app on their own Confluence site and drive agent runs against our allowlisted repos. The baseUrl body field is attacker-controlled and cannot bind the tenant; only the signature-verified clientKey (JWT iss) can. Added a MANDATORY, fail-closed CONNECT_EXPECTED_CLIENT_KEYS allowlist checked in process_install after signature+iss verification. HIGH — cross-tenant thread-id collision (CWE-330/863): Confluence comment ids are per-instance, so generate_thread_id_from_confluence_comment now salts the hash with the verified clientKey (plumbed from the webhook JWT iss) to prevent thread hijack across tenants. HIGH/MEDIUM — path/query injection (CWE-22/88): get_page and update_page interpolated page_id into the REST path unencoded (update_page on a mutating PUT with no params= backstop). Now _seg()-encoded, matching the rest of the module. MEDIUM — self-trigger loop (CWE-405): process_confluence_comment had no bot-authorship early-out. Added an optional CONFLUENCE_BOT_ACCOUNT_ID guard mirroring the Linear botActor / Jira comment_author_is_bot checks. LOW — corrected the CONNECT_EXPECTED_BASE_URL comment to document it as opt-in defense-in-depth (the clientKey allowlist is the real gate). Verified clean by the detectors: RS256/HS256 alg-pinning, aud/iss/exp, kid-fetch SSRF (host-pinned + quote-encoded), at-rest secret encryption, constant-time comparisons, and the Phase-2 hardening. New regression tests for each fix; full suite green (1602). * harden(open-swe): GPT-4.1 cross-family review follow-ups Cross-family review (GPT-4.1 via orchestrator cross_reviewer) found no critical/high issues and confirmed the auth boundary is fail-closed and correct. Two low-cost defense-in-depth items applied: - Validate the signed-install JWT 'kid' against a strict charset before the public-key fetch, so a malformed kid fails fast with no network call (on top of the existing fixed host + percent-encoding). - Make JWT nbf verification explicit (verify_nbf) on both the RS256 lifecycle and HS256 webhook decodes. Other suggestions triaged as already-handled (aud cross-app replay is blocked by the per-tenant iss->secret lookup; documented static-token/IP/ baseUrl tradeoffs; qsh pinned to Atlassian's vector) or ops/infra (Fernet rotation via MultiFernet; rate limiting at the gateway). * docs(open-swe): document Jira + Confluence in installation & customization guides - INSTALLATION.md §5: add Jira (Automation-rule webhook + shared secret, service account, JIRA_PROJECT_TO_REPO) and Confluence (Atlassian Connect app install, CONNECT_EXPECTED_CLIENT_KEYS bootstrap, durable-store note, CONFLUENCE_SPACE_TO_REPO) trigger setup; §6: add the new env vars + REQUIRE_REPO_ALLOWLIST. - CUSTOMIZATION.md: jira_*/confluence_* in the tools table; repo-extraction note covers all four sources. - AGENTS.md: match CLAUDE.md (triggers, webhooks, tool list, auth). - README.md: invocation section, tools table, and overview line.
2026-07-13 19:45:54 -04:00
if source == "jira":
jira_issue = ctx.get("jira_issue")
if isinstance(jira_issue, dict):
issue_key = jira_issue.get("key")
if issue_key:
await claim()
return await comment_on_jira_issue(issue_key, text)
return False
if source == "confluence":
confluence = ctx.get("confluence")
if isinstance(confluence, dict):
page_id = confluence.get("page_id")
if page_id:
await claim()
result = await add_confluence_comment(page_id, text)
return bool(result.get("success"))
return False
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
if source in ("github", "github_issue"):
repo_config = metadata.get("repo")
number = ctx.get("pr_number")
if number is None:
github_issue = ctx.get("github_issue")
if isinstance(github_issue, dict):
number = github_issue.get("number")
if isinstance(repo_config, dict) and isinstance(number, int):
token = await get_github_app_installation_token()
if token:
await claim()
return await post_github_comment(repo_config, number, text, token=token)
return False
logger.info("No failure-reply channel for thread %s (source=%s)", thread_id, source)
return False
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
def _dedup_key(payload: dict[str, Any]) -> str | None:
"""A run-distinguishing key for dedupe, or None when nothing distinguishes runs.
Prefers the platform's ``run_id`` (always present on real completion
webhooks). Manual/legacy payloads without one fall back to a marker built
from ``updated_at``/``created_at`` so a retry of the *same* run still dedupes
while a *different* failed run on the same thread still gets its reply — the
old sticky per-thread boolean silenced every later run forever (SR160-03).
"""
raw = payload.get("run_id")
if isinstance(raw, str) and raw:
return raw
for marker in ("updated_at", "created_at"):
value = payload.get(marker)
if isinstance(value, str) and value:
return f"{marker}:{value}"
return None
def _posted_failure_run_ids(metadata: dict[str, Any]) -> list[str]:
raw = metadata.get(_FAILURE_REPLY_RUN_IDS)
ids = [item for item in raw if isinstance(item, str) and item] if isinstance(raw, list) else []
latest = metadata.get(_FAILURE_REPLY_RUN_ID)
if isinstance(latest, str) and latest and latest not in ids:
ids.append(latest)
return ids
def _failure_reply_metadata(metadata: dict[str, Any], key: str) -> dict[str, Any]:
ids = [item for item in _posted_failure_run_ids(metadata) if item != key]
ids.append(key)
return {
_FAILURE_REPLY_RUN_ID: key,
_FAILURE_REPLY_RUN_IDS: ids[-_MAX_FAILURE_REPLY_RUN_IDS:],
}
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
async def handle_run_completion(payload: dict[str, Any]) -> dict[str, str]:
"""Handle a platform run-completion webhook POST.
Posts a failure reply only when the run ended in a failure state and we
haven't already replied for this thread.
"""
status = payload.get("status")
thread_id = payload.get("thread_id")
if not isinstance(thread_id, str) or not thread_id:
return {"status": "ignored", "reason": "missing thread_id"}
if status not in _TERMINAL_FAILURE_STATUSES:
return {"status": "ignored", "reason": f"non-failure status: {status}"}
client = langgraph_client()
try:
thread = await client.threads.get(thread_id)
except Exception: # noqa: BLE001
logger.warning("run-complete: could not load thread %s", thread_id, exc_info=True)
return {"status": "error", "reason": "thread fetch failed"}
metadata = thread.get("metadata") if isinstance(thread, dict) else None
metadata = metadata if isinstance(metadata, dict) else {}
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
key = _dedup_key(payload)
if key is not None and key in _posted_failure_run_ids(metadata):
return {"status": "ignored", "reason": "failure reply already posted for run"}
# Claim-then-post: record the dedup key immediately before the actual post
# (via the claim callback) so a retried/concurrent completion webhook can't
# double-post (SR160-02). The key is only claimed on a branch that delivers,
# so a thread with no reply channel never burns it. If the claim itself fails
# we raise so we skip the post, leaving the key unclaimed for a later retry
# rather than reporting a clean success that invites a duplicate. A payload
# with no distinguishing key can't be recorded; it posts un-deduped rather
# than being permanently suppressed.
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
async def _claim() -> None:
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
if key is None:
return
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
try:
feat: port durable dispatch hardening and startup latency improvements (#160) * feat: port plan-review & workflow-approval UX (#135) Port six upstream commits onto dev: - c03a6be7 (already ported): keep plan guidance high-level - 546042a4: add workflow approval UI with diff preview, approval URLs, web review links, and polling for approval status during active runs - 216cf181: remove workflow token elevation; approved pushes pass through directly without proxy token rewriting - 3dbc0282: preserve plan redirects after login by accepting relative same-origin redirect_to values and rejecting blocked paths - bb104d93: submit plan comments with cmd+enter - 90cb6caa: terse Slack replies, shared content via save_plan outside plan mode (PLAN_STATUS_SHARED), reject shared-content mutations Refs: #135 * feat: port durable dispatch hardening and startup latency improvements Port five upstream PRs onto dev: - #1621 / #1658: durable dispatch with loopback webhook defense, create_durable_run helper, _config_with_prepare_run_id, degradation to None for relative/loopback completion webhook URLs - #1696: run-level completion webhook deduplication (replace claim-then-post with post-then-flag per run_id), DeferredErrorModel for graph-factory resilience, ToolRetryMiddleware for task subagents, TimeoutWrapupMiddleware for all three graphs - #1697: lazy-load __init__.py for agent.middleware, agent.tools, agent.dashboard (PEP 562); defer heavy imports (exa_py in web_search, agent.webapp in request_pr_review, deepagents in sandbox.py); add ttl_cache.py with stale-while-revalidate for tool loaders Refs: #137 * fix: restore login page render and clear CI lint/format The plan-review port removed the authRedirectUrl import from login.tsx but left its call site, crashing the login page at runtime (blank page, no 'Sign in to open-swe'). Pass the relative path straight to loginUrl, matching the plan route and the backend relative-redirect handling. Also drop an unused os import in the guard test and reformat workflow_push_guard.py to satisfy ruff. * fix: restore RepairOrphaned middleware export and repoint model fake to deferred_model boundary * fix: restore RepairOrphanedToolCallsMiddleware, fix E2E model-fake patch, drop dead ttl_cache - Re-add RepairOrphanedToolCallsMiddleware to the lazy middleware __init__ (_MIDDLEWARE_MODULES, __all__, TYPE_CHECKING) so agent.reviewer can import it. - Reroute E2E model patching to deferred_model.make_model so make_model_or_defer (used by all three graph factories) returns the scripted fake instead of building a real model with fake credentials. - Drop unused agent/utils/ttl_cache.py — no agent module imports it. - Fix import ordering in agent/reviewer.py and agent/analyzer.py (ruff I001). - Format tests/test_dispatch.py. * fix: claim-then-post run-level failure dedup; stop permanent suppression --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com> Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-07-09 17:11:25 -04:00
await client.threads.update(
thread_id=thread_id,
metadata=_failure_reply_metadata(metadata, key),
)
refactor: adopt modular webhook architecture (#1621) + port fork customizations (#85) * Adopt upstream modular webhook skeleton (#1621) Apply the durable-interrupt-dispatch refactor: split the monolithic webapp.py into a thin routing layer plus per-source handlers in webhooks/{github,slack,linear}.py, and add completion.py, dispatch.py, and reconcile.py. Reconcile fork divergence by keeping the Bedrock/ Fireworks cross-provider fallback, the no-agent-attribution prompt policy, the dashboard-handoff re-export, and the Slack channel-info cache. ci_autofix is restored on the new dispatch model in a later commit. Refs: #80 * Port fork webhook security delta onto modular handlers Re-apply the fork's security customizations that #1621 did not carry: Linear webhook replay protection (freshness window on the signed webhookTimestamp), per-repo token-cache binding threaded through the thread token resolvers, the INTERNAL_BOT_LOGINS self-check in the review-finding-reply path, and a user-mapping cache refresh before email resolution on the issue and PR-comment paths (multi-replica staleness). Existing fork security tests pass unchanged. Refs: #80 * Restore CI auto-fix on the modular dispatch model Bring back ci_autofix.py and the ci_monitor graph that #1621 deleted, re-wiring the fork's security-reviewed PR-babysitting onto the new structure: the CI-event, autofix-toggle, and review-feedback handlers move into webhooks/github.py and the github_webhook router re-gains the check_run/check_suite/workflow_run/status routing plus the autofix command and actionable-review branches. Auto-fix runs now dispatch through dispatch_agent_run (durability + completion webhook) while keeping the deliberate batch-while-busy skip-rule via get_thread_active_status. Restore langgraph.json's ci_monitor entry and the fork autofix tests (dispatch mock + import paths re-pointed). Refs: #80 * Reformat and update docs for the modular webhook split Point CLAUDE.md and deploy/MIGRATION.md at the new webhooks/ modules and the dispatch/completion/reconcile contract, and mark the user-mapping cache-refresh fix as applied on the GitHub handlers. Refs: #80 * Restore reject backstop for autofix dispatch A burst of near-simultaneous CI events for one head SHA can slip past the busy-check before the dedupe SHA is recorded, so dispatch the autofix path with multitask_strategy=reject (dev's prior platform default) to drop duplicate concurrent creates instead of letting them interrupt each other. Also make the completion failure-reply dedup claim-then-post and drop the unreachable interrupted branch. --------- Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-06-30 18:46:46 -04:00
except Exception as exc: # noqa: BLE001
logger.warning("run-complete: could not flag thread %s", thread_id, exc_info=True)
raise _ClaimFailed from exc
try:
posted = await _post_failure_reply(thread_id, metadata, status, claim=_claim)
except _ClaimFailed:
return {"status": "error", "reason": "could not claim failure reply"}
if not posted:
return {"status": "ignored", "reason": "no reply posted"}
logger.info("Posted failure reply for thread %s (status=%s)", thread_id, status)
return {"status": "ok", "reason": "failure reply posted"}