mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
* wip(rebuild): core reliability spine
- remove PR-babysitting (ci_autofix + ci_monitor graph + webhook wiring)
- dispatch core: agent/dispatch.py with multitask_strategy=interrupt +
durability=sync + completion webhook; reroute all webhook + plan triggers;
drop the racy in-process lock + is_thread_active busy-check
- completion webhook: agent/completion.py + /webhooks/run-complete loopback
route for failure/timeout replies (idempotent)
Co-authored-by: open-swe[bot]
* feat(rebuild): async tools, reconcile, shared http timeouts, assembly tuning
Parallel batch on top of the reliability spine:
- async-ify all 24 tools (drop asyncio.run; requests->httpx); re-implement the
http_request/fetch_url SSRF + DNS-rebinding defense httpx-natively and harden
the IP check to 'not is_global' (+ IPv4-mapped unwrap)
- reconcile.py: stale pending-run sweep (threads.search -> per-thread runs.list
-> cancel_many), wired into the scheduler graph via task='reconcile'
- shared DEFAULT_HTTP_TIMEOUT (agent/utils/http.py) on every bare
httpx.AsyncClient() across utils/dashboard/webapp/middleware
- run budget: MODEL_CALL_RECURSION_LIMIT 5000->250
- fix stale OpenAI->Anthropic fallback id (claude-opus-4-5 -> 4-8)
- drop redundant custom repair middleware (deepagents auto-adds PatchToolCalls)
- confirm tool-result eviction + summarization auto-wired via backend
- slim system prompt ~8% (full harness-profile rewrite deferred)
Co-authored-by: open-swe[bot]
* feat(rebuild): harness-profile prompt + split webhooks out of webapp
- prompt.py: own the system prompt via a registered harness profile
(OPEN_SWE_SHARED_BASE, kept neutral so the read-only reviewer/analyzer that
share it stay safe), registered across all 4 providers; per-thread values
stay in construct_system_prompt. Assembled main-agent prompt ~6.8k -> ~3.1k
tokens (~55% smaller); de-duped PR/commit/suite/force-push guidance; dropped
ALL-CAPS markers.
- webapp.py 3325 -> 1890 LOC: moved 14 per-source handlers into
agent/webhooks/{linear,slack,github}.py; webapp re-exports them for the
routes + tests; moved handlers reach shared helpers via the webapp namespace
to preserve the test suite's monkeypatch targets.
Full suite: 1168 passing, lint clean.
Co-authored-by: open-swe[bot]
* Restore MODEL_CALL_RECURSION_LIMIT to 5000 for long-running tasks
Reverts the 250 cap from the run-budget change — long-running tasks legitimately
need many model calls. The notify_step_limit_reached safety net still fires if a
run does hit the cap, so runs end with a signal either way.
Co-authored-by: open-swe[bot]
* fix: address PR review (auth, SSRF, interrupted status, redirect headers)
- completion.py: drop `interrupted` from failure statuses — with
multitask_strategy=interrupt a follow-up ends the prior run as interrupted,
which is healthy, not a failure to report. [open-swe]
- /webhooks/run-complete: shared-secret auth — dispatch appends ?token= when
RUN_COMPLETE_WEBHOOK_SECRET is set; route verifies via hmac.compare_digest.
[corridor-security]
- SSRF: extract the URL validator to agent/utils/url_safety.py and apply it
before server-side image fetches in multimodal.fetch_image_block.
[corridor-security]
- http_request: preserve caller headers/extensions across redirect hops instead
of dropping them on the first hop. [open-swe]
Co-authored-by: open-swe[bot]
* chore: remove REBUILD_PLAN.md (planning doc, not needed in the repo)
Co-authored-by: open-swe[bot]
* fix: fail closed on run-complete webhook auth when secret unset
Corridor follow-up: verify_run_complete_token returns False (not True) when
RUN_COMPLETE_WEBHOOK_SECRET is unset, so the public route is never
unauthenticated. Logs a startup warning when the secret is absent, and dispatch
skips registering the webhook when there's no secret (no rejected callbacks).
Co-authored-by: open-swe[bot]
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
224 lines
9.1 KiB
Python
224 lines
9.1 KiB
Python
"""Tool: ``update_finding``. Mutate an existing finding by id."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from langgraph.config import get_config
|
|
|
|
from ..reviewer_findings import (
|
|
DEFAULT_FINDING_TITLE,
|
|
MAX_SUGGESTION_LINES,
|
|
Finding,
|
|
ReviewerThreadMissingError,
|
|
clip_suggestion,
|
|
get_thread_id_from_runtime,
|
|
list_findings,
|
|
normalize_finding_title,
|
|
resolve_review_head_sha,
|
|
thread_missing_tool_result,
|
|
update_finding_fields,
|
|
)
|
|
from ..utils.reviewer_outcomes import emit_finding_status_outcome
|
|
|
|
|
|
def _is_non_empty_str(value: Any) -> bool:
|
|
return isinstance(value, str) and bool(value)
|
|
|
|
|
|
def _normalize_note(note: str | None) -> str | None:
|
|
if note is None:
|
|
return None
|
|
normalized = note.strip()
|
|
return normalized or None
|
|
|
|
|
|
def _has_published_github_surface(finding: Finding) -> bool:
|
|
surface = finding.get("surface")
|
|
if isinstance(surface, dict) and (
|
|
isinstance(surface.get("github_review_comment_id"), int)
|
|
or _is_non_empty_str(surface.get("github_review_thread_id"))
|
|
):
|
|
return True
|
|
comment_ids = finding.get("github_review_comment_ids")
|
|
thread_ids = finding.get("github_review_thread_ids")
|
|
return (
|
|
isinstance(finding.get("github_review_comment_id"), int)
|
|
or _is_non_empty_str(finding.get("github_review_thread_id"))
|
|
or (isinstance(comment_ids, list) and any(isinstance(item, int) for item in comment_ids))
|
|
or (isinstance(thread_ids, list) and any(_is_non_empty_str(item) for item in thread_ids))
|
|
)
|
|
|
|
|
|
async def update_finding(
|
|
finding_id: str,
|
|
status: str | None = None,
|
|
severity: str | None = None,
|
|
confidence: str | None = None,
|
|
title: str | None = None,
|
|
description: str | None = None,
|
|
suggestion: str | None = None,
|
|
note: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Update fields on an existing finding.
|
|
|
|
Use this on a re-review run to mark an existing finding as resolved or
|
|
dismissed, or to revise its severity/description/suggestion if the new
|
|
commits changed the situation.
|
|
|
|
Args:
|
|
finding_id: The id returned by ``add_finding`` (or shown in the
|
|
``Existing findings`` block of the re-review user message).
|
|
status: New status (``open``, ``resolved``, ``dismissed``).
|
|
Use ``resolved`` when the new commits address the issue. Resolving
|
|
or dismissing requires a ``note`` with the message to post.
|
|
severity: New severity, if reassessing.
|
|
confidence: New confidence rating (``low``, ``medium``, ``high``), if
|
|
new commits change how sure you are the finding is a real issue.
|
|
title: New concise generated headline, if revising.
|
|
description: New description body, if revising. Do not repeat ``title``
|
|
as the first line.
|
|
suggestion: New replacement text. Pass an empty string to clear it.
|
|
Capped at 4 lines — longer values are dropped (the finding keeps
|
|
its description). Only set this for small, obvious fixes.
|
|
note: Optional free-form note explaining the change. Required when
|
|
resolving or dismissing because it becomes the GitHub reply body.
|
|
|
|
Returns:
|
|
Dictionary with ``success`` and (on success) the updated ``finding``.
|
|
"""
|
|
if status is not None and status not in {"open", "resolved", "dismissed"}:
|
|
return {"success": False, "error": f"Invalid status: {status}"}
|
|
if severity is not None and severity not in {"low", "medium", "high", "critical"}:
|
|
return {"success": False, "error": f"Invalid severity: {severity}"}
|
|
if confidence is not None and confidence not in {"low", "medium", "high"}:
|
|
return {"success": False, "error": f"Invalid confidence: {confidence}"}
|
|
normalized_note = _normalize_note(note)
|
|
if status in {"resolved", "dismissed"} and normalized_note is None:
|
|
return {
|
|
"success": False,
|
|
"error": "Resolving or dismissing a finding requires a note with the message to post.",
|
|
}
|
|
|
|
updates: dict[str, Any] = {}
|
|
suggestion_dropped = False
|
|
if status is not None:
|
|
updates["status"] = status
|
|
if severity is not None:
|
|
updates["severity"] = severity
|
|
if confidence is not None:
|
|
updates["confidence"] = confidence
|
|
if title is not None:
|
|
normalized_title = normalize_finding_title(title)
|
|
if normalized_title == DEFAULT_FINDING_TITLE:
|
|
return {"success": False, "error": "title must be a non-empty generated headline"}
|
|
updates["title"] = normalized_title
|
|
if description is not None:
|
|
updates["description"] = description
|
|
if suggestion is not None:
|
|
if suggestion == "":
|
|
updates["suggestion"] = None
|
|
else:
|
|
clipped, suggestion_dropped = clip_suggestion(suggestion)
|
|
if not suggestion_dropped:
|
|
updates["suggestion"] = clipped
|
|
if normalized_note is not None:
|
|
updates["last_update_note"] = normalized_note
|
|
if status in {"resolved", "dismissed"}:
|
|
updates["resolution_note"] = normalized_note
|
|
|
|
config = get_config()
|
|
configurable = config.get("configurable", {}) if isinstance(config, dict) else {}
|
|
if status == "open":
|
|
try:
|
|
head_sha = await resolve_review_head_sha(get_thread_id_from_runtime(), configurable)
|
|
except ReviewerThreadMissingError as exc:
|
|
return thread_missing_tool_result(exc)
|
|
if head_sha:
|
|
updates["last_confirmed_sha"] = head_sha
|
|
|
|
if not updates:
|
|
if suggestion_dropped:
|
|
return {
|
|
"success": False,
|
|
"suggestion_dropped": True,
|
|
"error": (
|
|
f"Suggestion exceeded the {MAX_SUGGESTION_LINES}-line cap "
|
|
"and was rejected; no other fields were provided, so "
|
|
"nothing was updated. Only include `suggestion` for "
|
|
"small, obvious fixes."
|
|
),
|
|
}
|
|
return {"success": False, "error": "No fields provided to update"}
|
|
|
|
thread_id = get_thread_id_from_runtime()
|
|
try:
|
|
findings = await list_findings(thread_id)
|
|
except ReviewerThreadMissingError as exc:
|
|
return thread_missing_tool_result(exc)
|
|
finding = next((item for item in findings if item.get("id") == finding_id), None)
|
|
if finding is None:
|
|
return {"success": False, "error": f"No finding found with id {finding_id}"}
|
|
|
|
delegated_resolution = False
|
|
repo_config = configurable.get("repo") if isinstance(configurable, dict) else None
|
|
pr_number = configurable.get("pr_number") if isinstance(configurable, dict) else None
|
|
can_resolve_github_thread = (
|
|
isinstance(repo_config, dict)
|
|
and bool(repo_config.get("owner"))
|
|
and bool(repo_config.get("name"))
|
|
and isinstance(pr_number, int)
|
|
)
|
|
if (
|
|
status in {"resolved", "dismissed"}
|
|
and can_resolve_github_thread
|
|
and _has_published_github_surface(finding)
|
|
):
|
|
from .resolve_finding_thread import resolve_finding_thread
|
|
|
|
resolve_result = await resolve_finding_thread(
|
|
finding_id, status=status, note=normalized_note
|
|
)
|
|
if not resolve_result.get("success"):
|
|
return {
|
|
"success": False,
|
|
"error": "GitHub review thread resolution failed; finding was left open.",
|
|
"github_resolution": resolve_result,
|
|
}
|
|
delegated_resolution = True
|
|
updates.pop("status", None)
|
|
updates.pop("last_update_note", None)
|
|
updates.pop("resolution_note", None)
|
|
if not updates:
|
|
result: dict[str, Any] = {
|
|
"success": True,
|
|
"finding": resolve_result.get("finding"),
|
|
"github_resolution": resolve_result,
|
|
}
|
|
if suggestion_dropped:
|
|
result["suggestion_dropped"] = True
|
|
result["warning"] = (
|
|
f"Suggestion exceeded the {MAX_SUGGESTION_LINES}-line cap and was "
|
|
"rejected — the finding's prior `suggestion` was left unchanged. "
|
|
"Only include `suggestion` for small, obvious fixes."
|
|
)
|
|
return result
|
|
|
|
try:
|
|
updated = await update_finding_fields(thread_id, finding_id, updates)
|
|
except ReviewerThreadMissingError as exc:
|
|
return thread_missing_tool_result(exc)
|
|
if updated is None:
|
|
return {"success": False, "error": f"No finding found with id {finding_id}"}
|
|
if status in {"resolved", "dismissed"} and not delegated_resolution:
|
|
emit_finding_status_outcome(updated, status, configurable=configurable, thread_id=thread_id)
|
|
result = {"success": True, "finding": updated}
|
|
if suggestion_dropped:
|
|
result["suggestion_dropped"] = True
|
|
result["warning"] = (
|
|
f"Suggestion exceeded the {MAX_SUGGESTION_LINES}-line cap and was "
|
|
"rejected — the finding's prior `suggestion` was left unchanged "
|
|
"and other fields were updated normally. Only include "
|
|
"`suggestion` for small, obvious fixes."
|
|
)
|
|
return result
|