mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-07 16:19:09 +00:00
Compare commits
3 commits
0f0f616cd4
...
c99bd78179
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c99bd78179 | ||
|
|
ac569b9a0a | ||
|
|
db05d5826f |
14 changed files with 418 additions and 96 deletions
|
|
@ -1,5 +1,25 @@
|
|||
{
|
||||
"suppressions": [
|
||||
{
|
||||
"id": "AUTHZ-CLEAN-AUTOAPPROVE-INJECTION-001",
|
||||
"title": "Clean-review auto-approve derives APPROVE authority from a model-controlled signal on the untrusted auto-review path (prompt-injection-mintable APPROVE)",
|
||||
"file": "agent/tools/publish_review.py",
|
||||
"severity": "high",
|
||||
"status": "confirmed",
|
||||
"suppression_justification": "ACCEPTED (Adam, 2026-07-21) as a knowingly-deferred risk merged into the dev integration branch via admin merge in PR #217. /sh-security-review returned BLOCK: moving `approve` authorization from the deterministic verdict_requested flag to open_findings_count==0 lets a prompt-injected auto-review (which never sets verdict_requested) land a real bot APPROVE on an attacker-controlled external/fork PR, potentially satisfying branch protection. This reintroduces the hole PR #214 closed. Merged to dev only (NOT main/prod). Compensating controls: (a) confirm dev does not auto-deploy to sh-openswe; (b) on sensitive repos, esp. payments-dashboard, configure rulesets so the seahaven-openswe[bot] APPROVE does not by itself satisfy required approvals. Tracked in issue #218 with the full redesign checklist. REVISIT TRIGGER: MUST be resolved before this change promotes from dev to main/prod, and immediately if dev is found to auto-deploy. Verified HIGH by the sh-security-review detector fan-out (6 independent detectors converged).",
|
||||
"owner": "adam@seahavenind.com",
|
||||
"added": "2026-07-21"
|
||||
},
|
||||
{
|
||||
"id": "AUTHZ-CLEAN-AUTOAPPROVE-THREADLAUNDER-002",
|
||||
"title": "Clean-review auto-approve gate reads reconcile-derived finding status, so a PR author can launder a dirty PR to clean via GitHub thread resolve/outdate",
|
||||
"file": "agent/tools/publish_review.py",
|
||||
"severity": "high",
|
||||
"status": "confirmed",
|
||||
"suppression_justification": "ACCEPTED (Adam, 2026-07-21), deferred with AUTHZ-CLEAN-AUTOAPPROVE-INJECTION-001 in PR #217 (admin-merged to dev). The open_findings_count gate reads finding status after reconcile_findings_with_review_threads, which flips open->resolved when the GitHub thread is is_resolved/is_outdated — both author-controllable ('Resolve conversation' or a trivial hunk-outdating commit) without fixing the defect, yielding an auto-approve on an unfixed PR. Same compensating controls and revisit trigger as ...-001. Tracked in issue #218 (redesign: compute the gate from the reviewer's own authoritative finding state, not author-influenced thread state). Verified HIGH by /sh-security-review.",
|
||||
"owner": "adam@seahavenind.com",
|
||||
"added": "2026-07-21"
|
||||
},
|
||||
{
|
||||
"id": "AUTHZ-SLACK-BOT-DEFAULT-001",
|
||||
"title": "Slack entrypoint lacks a per-user repo-access check; default-bot PR authoring removes the implicit per-user repo boundary",
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ The system prompt instructs the agent to call a tool every turn, and `ensure_no_
|
|||
|
||||
Other middleware exists in `agent/middleware/` (`ExcludeToolsMiddleware`) but isn't wired into the default agent. The reviewer uses a leaner stack (see `reviewer.py:get_reviewer_agent` for the authoritative order), including `SanitizeToolInputsMiddleware`, `ModelCallLimitMiddleware`, `ToolErrorMiddleware`, `PullRequestVerdictGuardMiddleware`, `SlackAssistantStatusMiddleware`, and `settle_review_check_on_exit`.
|
||||
|
||||
**Verdict gating (Sea Haven fork):** `PullRequestVerdictGuardMiddleware` (`agent/middleware/pr_verdict_guard.py`) is wired into BOTH graphs (`server.py:get_agent` after `PullRequestCreationGuardMiddleware`; `reviewer.py:get_reviewer_agent` after `ToolErrorMiddleware`). It blocks shell-path review verdicts (`gh pr review --approve/-a/--request-changes/-r`, `gh api`/`curl` posting `event=APPROVE|REQUEST_CHANGES` to `/pulls/N/reviews`); comment reviews and reads pass through. Verdicts go exclusively through `publish_review(verdict=...)`, which honors them only when the run's `configurable["verdict_requested"] is True` — set solely by the explicit-mention dispatch path (`trigger_pr_review_from_ref(request_verdict=True)` → `_build_reviewer_configurable`). Auto-review dispatches never set it. The tool layer also downgrades self-reviews (PR author in `INTERNAL_BOT_LOGINS`) to comment reviews and best-effort dismisses a recorded stale APPROVE when a later publish surfaces new findings.
|
||||
**Verdict gating (Sea Haven fork):** `PullRequestVerdictGuardMiddleware` (`agent/middleware/pr_verdict_guard.py`) is wired into BOTH graphs (`server.py:get_agent` after `PullRequestCreationGuardMiddleware`; `reviewer.py:get_reviewer_agent` after `ToolErrorMiddleware`). It blocks shell-path review verdicts (`gh pr review --approve/-a/--request-changes/-r`, `gh api`/`curl` posting `event=APPROVE|REQUEST_CHANGES` to `/pulls/N/reviews`); comment reviews and reads pass through. Verdicts go exclusively through `publish_review(verdict=...)`: `request_changes` is honored only when the run's `configurable["verdict_requested"] is True` — set solely by the explicit-mention dispatch path (`trigger_pr_review_from_ref(request_verdict=True)` → `_build_reviewer_configurable`); auto-review dispatches never set it. `approve` is additionally honored on non-verdict-requested runs when the review is clean (zero open findings — the clean-review auto-approve); with open findings it downgrades to a comment (`verdict_ignored_reason="approve_with_open_findings"`). The tool layer also downgrades self-reviews (PR author in `INTERNAL_BOT_LOGINS`) to comment reviews and best-effort dismisses a recorded stale APPROVE when a later publish surfaces new findings.
|
||||
|
||||
There is intentionally no after-agent safety net that opens a PR for the agent. The agent itself is responsible for committing, pushing, opening/updating the draft PR, and replying in the source channel — all via `GH_TOKEN=dummy gh` and `slack_thread_reply` / `linear_comment`.
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ Each invocation creates a deterministic thread ID, so follow-up messages on the
|
|||
|
||||
**Engineering conventions & attribution (Sea Haven fork):** the main agent's system prompt is tuned to the Sea Haven engineering handbook — branch names are `feature|bug|hotfix/<kebab-desc>` (optional resolvable `<KEY>-` prefix), PR bodies use `## Summary / Validation / Tests / Notes`, and commit messages follow the handbook format (≤50-char imperative subject, *why* over *what*). The **PR title rule is repo-aware**: when the target repo enforces a conventional-commit title (an `amannn/action-semantic-pull-request` workflow, a `commitlint` config, or a documented requirement in `AGENTS.md` / `CONTRIBUTING.md`), the agent emits a conforming `type(scope): …` title that reads the action's allowed types/scopes — this lets it pass gates like this repo's own `PR Title Lint` and upstream `langchain-ai/open-swe` without manual retitling; otherwise it falls back to the Sea Haven imperative style with no `type:` prefix. PRs that resolve a GitHub issue **auto-link it** in the body (`Closes #<n>` for full fixes, `Refs #<n>`/`Part of #<n>` for partial work, `Closes owner/repo#<n>` cross-repo); because the Sea Haven flow targets `dev` rather than the default branch, the issue closes when `dev` is promoted, not at dev-merge. **No agent/AI attribution is added to any artifact** — no `Co-authored-by` bot trailer, no `Made by [Open SWE]` footer, no "generated by an agent" notes. Commits are currently authored as the **triggering user** (the upstream behavior, which keeps Vercel preview deploys resolvable); flipping authorship to the bot account is tracked separately in issue #11 pending the Vercel-resolvability decision.
|
||||
|
||||
**PR reviews & verdicts (Sea Haven fork):** a separate read-only **reviewer** graph reviews PRs and publishes findings as a GitHub Review. Auto-reviews (PR opened / ready-for-review / push re-review / finding replies) are always **advisory** (`event=COMMENT`). When a user's `@openswe` mention *explicitly asks for a verdict* ("approve if it meets the bar; request changes if not"), the coding agent forwards the request via `request_pr_review(instructions=..., request_verdict=True)`; the reviewer run is then authorized — enforced in code via `configurable["verdict_requested"]`, not prompt — to submit a real **APPROVE** or **REQUEST_CHANGES** through `publish_review(verdict=...)`. Requester instructions travel as an escaped `<requester_instructions>` data block (untrusted: they may set focus/merge bar, never override safety rules). Safeguards: verdicts on Open SWE's own PRs are downgraded to comment reviews (self-review guard); unauthorized verdicts are dropped with `verdict_ignored`; a recorded APPROVE is best-effort dismissed when a later review surfaces new findings; and `PullRequestVerdictGuardMiddleware` blocks the shell path on both graphs.
|
||||
**PR reviews & verdicts (Sea Haven fork):** a separate read-only **reviewer** graph reviews PRs and publishes findings as a GitHub Review. Auto-reviews (PR opened / ready-for-review / push re-review / finding replies) with findings are **advisory** (`event=COMMENT`) — but a **clean review (zero open findings) auto-approves**: the reviewer calls `publish_review(verdict="approve")` and it lands as a real **APPROVE** even without an explicit verdict request. When a user's `@openswe` mention *explicitly asks for a verdict* ("approve if it meets the bar; request changes if not"), the coding agent forwards the request via `request_pr_review(instructions=..., request_verdict=True)`; the reviewer run is then authorized — enforced in code via `configurable["verdict_requested"]`, not prompt — to submit a real **APPROVE** or **REQUEST_CHANGES** through `publish_review(verdict=...)`. Requester instructions travel as an escaped `<requester_instructions>` data block (untrusted: they may set focus/merge bar, never override safety rules). Safeguards: verdicts on Open SWE's own PRs are downgraded to comment reviews (self-review guard); unauthorized `request_changes` verdicts are dropped with `verdict_ignored`, and an unsolicited approve alongside open findings is downgraded to a comment (`approve_with_open_findings`); a recorded APPROVE is best-effort dismissed when a later review surfaces new findings; and `PullRequestVerdictGuardMiddleware` blocks the shell path on both graphs.
|
||||
|
||||
### 7. Validation — Prompt-Driven
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import hmac
|
|||
import logging
|
||||
import os
|
||||
from typing import Any, Literal
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request
|
||||
|
|
@ -377,12 +378,14 @@ async def auth_login(
|
|||
nonce_hash=hash_state_nonce(nonce),
|
||||
)
|
||||
redirect_uri = f"{_api_base_url()}/dashboard/api/auth/callback"
|
||||
url = (
|
||||
"https://github.com/login/oauth/authorize"
|
||||
f"?client_id={client_id}"
|
||||
f"&redirect_uri={redirect_uri}"
|
||||
f"&state={state}"
|
||||
query = urlencode(
|
||||
{
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"state": state,
|
||||
}
|
||||
)
|
||||
url = f"https://github.com/login/oauth/authorize?{query}"
|
||||
response = RedirectResponse(url, status_code=302)
|
||||
_set_state_cookie(response, nonce)
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -299,8 +299,16 @@ severities — they're not findings.
|
|||
|
||||
- Read-only. Do not commit, push, or use `gh pr review` / `gh api .../reviews`.
|
||||
Never approve or request changes on a PR via the shell or the GitHub API —
|
||||
review verdicts go exclusively through `publish_review`, and only when this
|
||||
run was explicitly authorized to submit one.
|
||||
review verdicts go exclusively through `publish_review`. A
|
||||
`request_changes` verdict is honored only when this run was explicitly
|
||||
authorized to submit one.
|
||||
- Clean-review auto-approve: when your finished review has zero open
|
||||
findings and the diff meets a normal production merge bar, call
|
||||
`publish_review(verdict="approve")` even without an explicit verdict
|
||||
request — clean reviews land as real approvals. Do not pass a verdict
|
||||
while open findings remain unless this run was explicitly authorized to
|
||||
submit one; an unsolicited approve alongside open findings is downgraded
|
||||
to a comment review.
|
||||
- One finding per defect (with the fan-out rule above for cross-file bugs).
|
||||
- Include `suggestion` only when the fix is ≤4 lines and obvious.
|
||||
- Publish a concise review: prefer the highest-confidence findings that
|
||||
|
|
|
|||
|
|
@ -4,21 +4,23 @@ from ..utils.linear import search_issues
|
|||
|
||||
|
||||
async def linear_search_issues(
|
||||
query: str,
|
||||
query: str | None = None,
|
||||
team_id: str | None = None,
|
||||
filters: dict[str, Any] | None = None,
|
||||
limit: int = 10,
|
||||
include_archived: bool = False,
|
||||
include_comments: bool = False,
|
||||
after: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Search Linear issues by title, description, and optionally comments.
|
||||
"""Search Linear issues by text, structured filters, or both.
|
||||
|
||||
Args:
|
||||
query: Free-text search query.
|
||||
query: Optional free-text query over issue content.
|
||||
team_id: Optional team UUID used to restrict matches to that team.
|
||||
filters: Optional Linear IssueFilter object for labels, state, project, assignee, and more.
|
||||
limit: Maximum results to return, from 1 to 50.
|
||||
include_archived: Whether to include archived issues.
|
||||
include_comments: Whether to search issue comments in addition to issue content.
|
||||
include_comments: Whether free-text search includes issue comments.
|
||||
after: Optional pagination cursor from a previous result's page_info.endCursor.
|
||||
|
||||
Returns:
|
||||
|
|
@ -27,6 +29,7 @@ async def linear_search_issues(
|
|||
return await search_issues(
|
||||
query=query,
|
||||
team_id=team_id,
|
||||
filters=filters,
|
||||
limit=limit,
|
||||
include_archived=include_archived,
|
||||
include_comments=include_comments,
|
||||
|
|
|
|||
|
|
@ -90,12 +90,16 @@ async def publish_review(
|
|||
mentioned in the review summary with a link to the web app, but are
|
||||
not posted as inline PR comments.
|
||||
verdict: Optional review verdict — ``"approve"`` or
|
||||
``"request_changes"``. Honored ONLY when this run was explicitly
|
||||
authorized to submit a verdict (the triggering user asked for one);
|
||||
otherwise the review is published as a plain comment and the result
|
||||
carries ``verdict_ignored: true``. Never describe an ignored
|
||||
verdict as an approval. Verdicts are also downgraded to a comment
|
||||
when the PR was authored by Open SWE itself (self-review).
|
||||
``"request_changes"``. ``"request_changes"`` is honored ONLY when
|
||||
this run was explicitly authorized to submit a verdict (the
|
||||
triggering user asked for one). ``"approve"`` is also honored on a
|
||||
run without that authorization when the review is clean — zero
|
||||
open findings — so a clean review lands as a real APPROVE; with
|
||||
open findings an unsolicited approve is downgraded to a plain
|
||||
comment and the result carries ``verdict_ignored: true``. Never
|
||||
describe an ignored verdict as an approval. Verdicts are also
|
||||
downgraded to a comment when the PR was authored by Open SWE
|
||||
itself (self-review).
|
||||
Returns:
|
||||
Dictionary with ``success``, ``review_id``, ``surfaced_count``,
|
||||
``hidden_count``, ``resolved_thread_count``, and sometimes
|
||||
|
|
@ -119,8 +123,9 @@ async def publish_review(
|
|||
``verdict_submitted`` (GitHub confirmed the requested APPROVE/
|
||||
REQUEST_CHANGES state) or ``verdict_ignored`` +
|
||||
``verdict_ignored_reason`` (``"verdict_not_requested"``,
|
||||
``"self_review"``, ``"head_moved"`` — the reviewed commit is no longer
|
||||
the PR head — or ``"author_unknown"``).
|
||||
``"approve_with_open_findings"`` — an unsolicited approve on a run
|
||||
with open findings — ``"self_review"``, ``"head_moved"`` — the
|
||||
reviewed commit is no longer the PR head — or ``"author_unknown"``).
|
||||
"""
|
||||
if severity_threshold not in {"low", "medium", "high", "critical"}:
|
||||
return {"success": False, "error": f"Invalid severity_threshold: {severity_threshold}"}
|
||||
|
|
@ -179,18 +184,25 @@ async def publish_review(
|
|||
|
||||
# Verdict authorization is enforced here in code, not in the prompt: only
|
||||
# a run whose dispatching webhook set verdict_requested (the explicit
|
||||
# mention path) may submit a real review state. Anything else — including
|
||||
# a model that hallucinates authorization — publishes as a plain comment.
|
||||
# mention path) may submit a blocking review state. Anything else —
|
||||
# including a model that hallucinates authorization — publishes as a plain
|
||||
# comment. Exception: an unsolicited "approve" is allowed through, and
|
||||
# _publish_review_async honors it only when the review has zero open
|
||||
# findings (clean-review auto-approve).
|
||||
verdict_not_requested = False
|
||||
unsolicited_approve = False
|
||||
if verdict is not None and configurable.get("verdict_requested") is not True:
|
||||
logger.info(
|
||||
"publish_review verdict %r dropped: run not authorized to submit verdicts "
|
||||
"(pr_number=%s)",
|
||||
verdict,
|
||||
pr_number,
|
||||
)
|
||||
verdict = None
|
||||
verdict_not_requested = True
|
||||
if verdict == "approve":
|
||||
unsolicited_approve = True
|
||||
else:
|
||||
logger.info(
|
||||
"publish_review verdict %r dropped: run not authorized to submit verdicts "
|
||||
"(pr_number=%s)",
|
||||
verdict,
|
||||
pr_number,
|
||||
)
|
||||
verdict = None
|
||||
verdict_not_requested = True
|
||||
|
||||
try:
|
||||
result = await _publish_review_async(
|
||||
|
|
@ -206,6 +218,7 @@ async def publish_review(
|
|||
trace_link_config_override=configurable.get("review_trace_link_enabled"),
|
||||
verdict=verdict,
|
||||
verdict_requester=str(configurable.get("github_login") or ""),
|
||||
unsolicited_approve=unsolicited_approve,
|
||||
)
|
||||
if verdict_not_requested:
|
||||
result["verdict_ignored"] = True
|
||||
|
|
@ -310,6 +323,7 @@ async def _publish_review_async(
|
|||
trace_link_config_override: object = None,
|
||||
verdict: str | None = None,
|
||||
verdict_requester: str = "",
|
||||
unsolicited_approve: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
thread_id = get_thread_id_from_runtime()
|
||||
verdict_ignored_reason: str | None = None
|
||||
|
|
@ -370,9 +384,6 @@ async def _publish_review_async(
|
|||
)
|
||||
verdict = None
|
||||
verdict_ignored_reason = "self_review"
|
||||
event = _VERDICT_EVENTS.get(verdict or "", "COMMENT")
|
||||
review_trace_url = await _resolve_review_trace_url(thread_id, trace_link_config_override)
|
||||
review_ui_url = dashboard_review_url(owner, repo, pr_number)
|
||||
findings = await _backfill_findings_from_pr_threads(
|
||||
thread_id=thread_id,
|
||||
owner=owner,
|
||||
|
|
@ -381,6 +392,28 @@ async def _publish_review_async(
|
|||
token=token,
|
||||
)
|
||||
|
||||
# An unsolicited approve (no verdict_requested on the run) is honored only
|
||||
# for a clean review: any open finding — new, previously published, or
|
||||
# below the surfacing threshold — means changes are effectively being
|
||||
# requested, so the approve downgrades to a comment.
|
||||
if verdict == "approve" and unsolicited_approve:
|
||||
open_findings_count = sum(1 for f in findings if f.get("status", "open") == "open")
|
||||
if open_findings_count:
|
||||
logger.info(
|
||||
"publish_review unsolicited approve downgraded to comment: %d open "
|
||||
"finding(s) for %s/%s#%s",
|
||||
open_findings_count,
|
||||
owner,
|
||||
repo,
|
||||
pr_number,
|
||||
)
|
||||
verdict = None
|
||||
verdict_ignored_reason = "approve_with_open_findings"
|
||||
|
||||
event = _VERDICT_EVENTS.get(verdict or "", "COMMENT")
|
||||
review_trace_url = await _resolve_review_trace_url(thread_id, trace_link_config_override)
|
||||
review_ui_url = dashboard_review_url(owner, repo, pr_number)
|
||||
|
||||
# Re-reviews only post NEW findings. Anything with a github_review_comment_id
|
||||
# already lives on GitHub from a prior publish — reposting would create
|
||||
# duplicate inline comments and break the resolve-on-fix flow (only
|
||||
|
|
@ -489,6 +522,7 @@ async def _publish_review_async(
|
|||
verdict=verdict,
|
||||
verdict_requester=verdict_requester,
|
||||
verdict_ignored_reason=verdict_ignored_reason,
|
||||
unsolicited_approve=unsolicited_approve,
|
||||
)
|
||||
|
||||
review_response = await post_pull_request_review(
|
||||
|
|
@ -530,6 +564,7 @@ async def _publish_review_async(
|
|||
verdict=verdict,
|
||||
verdict_requester=verdict_requester,
|
||||
verdict_ignored_reason=verdict_ignored_reason,
|
||||
unsolicited_approve=unsolicited_approve,
|
||||
)
|
||||
retry_response = await post_pull_request_review(
|
||||
owner=owner,
|
||||
|
|
@ -577,6 +612,7 @@ async def _publish_review_async(
|
|||
verdict=verdict,
|
||||
verdict_requester=verdict_requester,
|
||||
verdict_ignored_reason=verdict_ignored_reason,
|
||||
unsolicited_approve=unsolicited_approve,
|
||||
)
|
||||
verdict_only_response = await post_pull_request_review(
|
||||
owner=owner,
|
||||
|
|
@ -806,9 +842,12 @@ def _decorate_review_body(
|
|||
verdict: str | None,
|
||||
verdict_requester: str,
|
||||
verdict_ignored_reason: str | None,
|
||||
unsolicited_approve: bool = False,
|
||||
) -> str:
|
||||
"""Append verdict attribution / downgrade context to the review body."""
|
||||
if verdict is not None:
|
||||
if unsolicited_approve:
|
||||
return f"{body}\n\nVerdict (`approve`) submitted — the review found no open issues."
|
||||
requester = f"@{verdict_requester}" if verdict_requester else "the requester"
|
||||
return f"{body}\n\nVerdict (`{verdict}`) submitted at the request of {requester}."
|
||||
if verdict_ignored_reason == "self_review":
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ def verify_github_signature(body: bytes, signature: str, *, secret: str) -> bool
|
|||
logger.warning("GITHUB_WEBHOOK_SECRET is not configured — rejecting webhook request")
|
||||
return False
|
||||
|
||||
if not signature:
|
||||
return False
|
||||
|
||||
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
||||
return hmac.compare_digest(expected, signature)
|
||||
|
||||
|
|
|
|||
|
|
@ -133,76 +133,110 @@ async def get_issue(issue_id: str) -> dict[str, Any]:
|
|||
|
||||
|
||||
async def search_issues(
|
||||
query: str,
|
||||
query: str | None = None,
|
||||
team_id: str | None = None,
|
||||
filters: dict[str, Any] | None = None,
|
||||
limit: int = 10,
|
||||
include_archived: bool = False,
|
||||
include_comments: bool = False,
|
||||
after: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Search Linear issues by free-text query."""
|
||||
query = query.strip()
|
||||
if not query:
|
||||
return {"error": "Search query must not be empty"}
|
||||
"""Search Linear issues by text, structured filters, or both."""
|
||||
query = (query or "").strip()
|
||||
issue_filter = dict(filters or {})
|
||||
if team_id:
|
||||
team_filter = {"team": {"id": {"eq": team_id}}}
|
||||
issue_filter = {"and": [issue_filter, team_filter]} if issue_filter else team_filter
|
||||
if not query and not issue_filter:
|
||||
return {"error": "Search query or filters must be provided"}
|
||||
if not 1 <= limit <= 50:
|
||||
return {"error": "Search limit must be between 1 and 50"}
|
||||
|
||||
search_query = """
|
||||
query SearchIssues(
|
||||
$query: String!
|
||||
$filter: IssueFilter
|
||||
$limit: Int!
|
||||
$includeArchived: Boolean
|
||||
$includeComments: Boolean
|
||||
$after: String
|
||||
) {
|
||||
searchIssues(
|
||||
term: $query
|
||||
filter: $filter
|
||||
first: $limit
|
||||
includeArchived: $includeArchived
|
||||
includeComments: $includeComments
|
||||
after: $after
|
||||
) {
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
nodes {
|
||||
id
|
||||
identifier
|
||||
title
|
||||
priority
|
||||
priorityLabel
|
||||
state { id name type }
|
||||
assignee { id name email }
|
||||
team { id name key }
|
||||
project { id name }
|
||||
labels { nodes { id name } }
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
url
|
||||
}
|
||||
connection_fields = """
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
nodes {
|
||||
id
|
||||
identifier
|
||||
title
|
||||
priority
|
||||
priorityLabel
|
||||
state { id name type }
|
||||
assignee { id name email }
|
||||
team { id name key }
|
||||
project { id name }
|
||||
labels { nodes { id name } }
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
url
|
||||
}
|
||||
}
|
||||
"""
|
||||
result = await _graphql_request(
|
||||
search_query,
|
||||
{
|
||||
if query:
|
||||
graphql_query = f"""
|
||||
query SearchIssues(
|
||||
$query: String!
|
||||
$filter: IssueFilter
|
||||
$limit: Int!
|
||||
$includeArchived: Boolean
|
||||
$includeComments: Boolean
|
||||
$after: String
|
||||
) {{
|
||||
searchIssues(
|
||||
term: $query
|
||||
filter: $filter
|
||||
first: $limit
|
||||
includeArchived: $includeArchived
|
||||
includeComments: $includeComments
|
||||
after: $after
|
||||
) {{
|
||||
{connection_fields}
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
variables = {
|
||||
"query": query,
|
||||
"filter": {"team": {"id": {"eq": team_id}}} if team_id else None,
|
||||
"filter": issue_filter or None,
|
||||
"limit": limit,
|
||||
"includeArchived": include_archived,
|
||||
"includeComments": include_comments,
|
||||
"after": after,
|
||||
},
|
||||
)
|
||||
}
|
||||
connection_name = "searchIssues"
|
||||
else:
|
||||
graphql_query = f"""
|
||||
query FilterIssues(
|
||||
$filter: IssueFilter!
|
||||
$limit: Int!
|
||||
$includeArchived: Boolean
|
||||
$after: String
|
||||
) {{
|
||||
issues(
|
||||
filter: $filter
|
||||
first: $limit
|
||||
includeArchived: $includeArchived
|
||||
after: $after
|
||||
) {{
|
||||
{connection_fields}
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
variables = {
|
||||
"filter": issue_filter,
|
||||
"limit": limit,
|
||||
"includeArchived": include_archived,
|
||||
"after": after,
|
||||
}
|
||||
connection_name = "issues"
|
||||
|
||||
result = await _graphql_request(graphql_query, variables)
|
||||
if "error" in result:
|
||||
return result
|
||||
|
||||
search_results = result.get("searchIssues", {})
|
||||
search_results = result.get(connection_name, {})
|
||||
return {
|
||||
"issues": search_results.get("nodes", []),
|
||||
"total_count": search_results.get("totalCount", 0),
|
||||
|
|
|
|||
|
|
@ -1164,6 +1164,9 @@ def verify_linear_signature(body: bytes, signature: str, secret: str) -> bool:
|
|||
logger.warning("LINEAR_WEBHOOK_SECRET is not configured — rejecting webhook request")
|
||||
return False
|
||||
|
||||
if not signature:
|
||||
return False
|
||||
|
||||
expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(expected, signature):
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{"_meta": {"last_synced": "3ea29d3f", "last_synced_date": "2026-07-20"}}
|
||||
{"_meta": {"last_synced": "1443fc4b", "last_synced_date": "2026-07-21"}}
|
||||
{"sha": "0b76afdc", "pr": 1653, "subject": "reviews block agenda, sticky headers, diff scroll", "disposition": "landed", "reason": "", "branch": "cherry-pick-upstream", "local_sha": null, "updated": "2026-07-02T00:00:00Z"}
|
||||
{"sha": "7530653b", "pr": 1655, "subject": "ResizeObserver settle for review scroll-to", "disposition": "landed", "reason": "", "branch": "cherry-pick-upstream", "local_sha": null, "updated": "2026-07-02T00:00:00Z"}
|
||||
{"sha": "23bd4a63", "pr": 1660, "subject": "top padding to sticky review block header", "disposition": "landed", "reason": "", "branch": "cherry-pick-upstream", "local_sha": null, "updated": "2026-07-02T00:00:00Z"}
|
||||
|
|
@ -124,9 +124,22 @@
|
|||
{"sha": "dccf6437", "pr": 1769, "subject": "fix: tighten sandbox config test types (#1769)", "disposition": "wont-merge", "reason": "test-only change in post-reorg path tests/sandbox/ — fork doesn't have this file (domain reorg #1726 deferred)", "branch": "", "local_sha": null, "updated": "2026-07-17T12:42:37Z"}
|
||||
{"sha": "c9a193e2", "pr": 1766, "subject": "chore(deps): bump mcp from 1.27.2 to 1.28.1 (#1766)", "disposition": "wont-merge", "reason": "indirect dependency bump (mcp); fork's own Dependabot handles these", "branch": "", "local_sha": null, "updated": "2026-07-17T12:42:37Z"}
|
||||
{"sha": "d0b63551", "pr": 1773, "subject": "fix: remove workflow push approval gating (#1773)", "disposition": "wont-merge", "reason": "Removes WorkflowPushGuardMiddleware and the proxy-token permission ladder — both actively wired in this fork (server.py middleware stack; github_app.py scoped-mint fallback). Adopting would let agent runs push .github/workflows/ changes with no human approval and mint proxy tokens at full scope unconditionally — a security-posture loosening counter to Sea Haven gating. Keep the fork's guard; skip the doc/prompt relaxation too (fork prompt documents the approval flow).", "branch": "", "local_sha": null, "updated": "2026-07-17T22:33:45Z"}
|
||||
{"sha": "b5e52925", "pr": 1775, "subject": "feat: add structured Linear issue filters (#1775)", "disposition": "deferred", "reason": "Clean pick: structured filters for linear_search_issues — stacks directly on #1748 (landed PR #206); zero fork drift on all three files. Ready whenever.", "branch": "linear-tooling", "local_sha": null, "updated": "2026-07-17T22:33:45Z"}
|
||||
{"sha": "b5e52925", "pr": 1775, "subject": "feat: add structured Linear issue filters (#1775)", "disposition": "landed", "reason": "Clean pick: structured filters for linear_search_issues — stacks directly on #1748 (landed PR #206); zero fork drift on all three files. Ready whenever.", "branch": "feature/upstream-clean-batch-security-linear", "local_sha": null, "updated": "2026-07-20T19:46:00Z"}
|
||||
{"sha": "81d544bc", "pr": 1765, "subject": "feat: Expose more specific AGENTS.md context to Open SWE reviewer (#1765)", "disposition": "deferred", "reason": "Near-clean: agents_md.py helper + tests are zero-drift; reviewer.py hunk is small (~39 lines) but lands in the fork's heavily-diverged reviewer — hand-apply the scoped_agents_md wiring onto the fork's fetch_agents_md call sites (reviewer.py ~L404/445/1031).", "branch": "reviewer-context", "local_sha": null, "updated": "2026-07-17T22:33:45Z"}
|
||||
{"sha": "8c8e58bc", "pr": 1776, "subject": "feat: connect automations to Slack channels (#1776)", "disposition": "deferred", "reason": "Moderate reconcile: Slack-channel wiring for automations. Fork helpers exist (post_slack_top_level_message_with_ts, generate_thread_id_from_slack_thread, slack_id_for_login); completion.py (+233 drift) and schedules.py (+167) need hand-merge; UI automations feature present. Keep backend+UI+tests as one vertical.", "branch": "automations-slack", "local_sha": null, "updated": "2026-07-17T22:33:46Z"}
|
||||
{"sha": "f0897479", "pr": 1778, "subject": "feat: surface context window usage in agents UI (#1778)", "disposition": "deferred", "reason": "Near-clean: context-window indicator UI is all new files (zero drift); options.py hunk must be re-keyed to the fork's Bedrock/Fireworks model map (fork model IDs differ from upstream's) — add context_window per fork entry rather than taking upstream values.", "branch": "context-usage-ui", "local_sha": null, "updated": "2026-07-17T22:33:46Z"}
|
||||
{"sha": "31263f83", "pr": 1785, "subject": "Fix: Fix Stored XSS in ReplyCard.tsx (#1785)", "disposition": "deferred", "reason": "SECURITY priority, near-clean: diff is urlencode() hardening of the GitHub OAuth authorize URL in dashboard routes.py auth_login (upstream bot title says ReplyCard.tsx — mismatch, trust the diff). Fork auth_login has the identical f-string URL; hunk applies clean. Port promptly.", "branch": "sec-oauth-urlencode", "local_sha": null, "updated": "2026-07-20T18:06:01Z"}
|
||||
{"sha": "3ea29d3f", "pr": 1789, "subject": "Fix: Fix Improper privilege management in server.py (#1789)", "disposition": "deferred", "reason": "SECURITY priority, near-clean: adds empty-signature reject to verify_github_signature (utils/github_comments.py) + verify_linear_signature (webhooks/common.py) (title says server.py — mismatch, trust the diff). Both fork functions match at the hunk sites; fork-only verify_jira_secret already guards empty token. Real value: None signature currently raises TypeError in compare_digest. Port BOTH hunks together.", "branch": "sec-webhook-empty-sig", "local_sha": null, "updated": "2026-07-20T18:06:01Z"}
|
||||
{"sha": "31263f83", "pr": 1785, "subject": "Fix: Fix Stored XSS in ReplyCard.tsx (#1785)", "disposition": "landed", "reason": "SECURITY priority, near-clean: diff is urlencode() hardening of the GitHub OAuth authorize URL in dashboard routes.py auth_login (upstream bot title says ReplyCard.tsx — mismatch, trust the diff). Fork auth_login has the identical f-string URL; hunk applies clean. Port promptly.", "branch": "feature/upstream-clean-batch-security-linear", "local_sha": null, "updated": "2026-07-20T19:46:00Z"}
|
||||
{"sha": "3ea29d3f", "pr": 1789, "subject": "Fix: Fix Improper privilege management in server.py (#1789)", "disposition": "landed", "reason": "SECURITY priority, near-clean: adds empty-signature reject to verify_github_signature (utils/github_comments.py) + verify_linear_signature (webhooks/common.py) (title says server.py — mismatch, trust the diff). Both fork functions match at the hunk sites; fork-only verify_jira_secret already guards empty token. Real value: None signature currently raises TypeError in compare_digest. Port BOTH hunks together.", "branch": "feature/upstream-clean-batch-security-linear", "local_sha": null, "updated": "2026-07-20T19:46:00Z"}
|
||||
{"sha": "2e8ff4b7", "pr": 1782, "subject": "chore: clarify shared response image guidance (#1782)", "disposition": "deferred", "reason": "Near-clean: docstring-only guidance in save_plan (shared responses persist Markdown only, never sandbox-local images — post screenshots directly to Slack) + 2 assertions in test_plan_review. Merges clean against fork. Batch with the next clean-port round.", "branch": "feature/upstream-clean-batch-jul21", "local_sha": null, "updated": "2026-07-21T18:00:13Z"}
|
||||
{"sha": "4ea2441a", "pr": 1791, "subject": "fix: match embedded review description background (#1791)", "disposition": "deferred", "reason": "Near-clean UI-only: ReviewMainBody.tsx background match for the embedded review description; merges clean, zero fork drift. Batch with the next clean-port round.", "branch": "feature/upstream-clean-batch-jul21", "local_sha": null, "updated": "2026-07-21T18:00:13Z"}
|
||||
{"sha": "589dfd83", "pr": 1787, "subject": "fix: Harden Stagehand browser URL handling (#1787)", "disposition": "wont-merge", "reason": "Follows #1648 (wont-merge): fork does not carry the Stagehand browser subagent — this 'fix' re-adds the entire module (992 insertions; modify/delete vs HEAD) plus the stagehand dep in pyproject/uv.lock. Adopting would resurrect a feature rejected under the curated-tools policy.", "branch": "", "local_sha": null, "updated": "2026-07-21T18:00:14Z"}
|
||||
{"sha": "9bbd65d3", "pr": 1781, "subject": "fix: derive model context windows from LangChain profiles (#1781)", "disposition": "deferred", "reason": "Pair with deferred #1778 on context-usage-ui: derives model context windows from LangChain profiles. options.py conflicts — re-key to the fork's Bedrock/Fireworks model map (upstream IDs differ; verify LangChain profiles even resolve for the fork's Bedrock-style IDs, else keep static values). uv.lock bumps langchain to a profiles-capable version; test_model_fallback_resolution also conflicts. Port with/after #1778.", "branch": "context-usage-ui", "local_sha": null, "updated": "2026-07-21T18:00:14Z"}
|
||||
{"sha": "df743658", "pr": 1774, "subject": "feat: add repository skill support to the coding agent (#1774)", "disposition": "deferred", "reason": "New feature: loads repo-provided skills into the coding agent (TrustedSkillsMiddleware, a deepagents SkillsMiddleware subclass pinned to a trusted ref, + repo_prep discovery). SECURITY review required before adopting — skills are repo-controlled prompt content (injection surface); verify the trusted-ref pinning against the fork threat model before wiring. Only server.py conflicts (fork tool/middleware wiring — re-key stack order); trusted_skills.py and repo_prep.py hunks merge clean.", "branch": "", "local_sha": null, "updated": "2026-07-21T18:00:23Z"}
|
||||
{"sha": "75fb8b48", "pr": 1786, "subject": "Fix PR creation guard shell bypasses (#1786)", "disposition": "deferred", "reason": "SECURITY priority, clean merge: closes shell-bypass holes in PullRequestCreationGuardMiddleware (nested 'bash -c' expansion to depth 3, quoted-executable normalization) — a guard this fork actively wires. Port promptly; ALSO mirror the nested-shell expansion into the fork-only PullRequestVerdictGuardMiddleware (pr_verdict_guard.py), which shares the naive shlex approach and has the same bypass shape.", "branch": "feature/upstream-clean-batch-jul21", "local_sha": null, "updated": "2026-07-21T18:00:23Z"}
|
||||
{"sha": "32e81f29", "pr": 1788, "subject": "Fix: Fix Insecure Direct Object Reference in slack_start_new_thread.py (#1788)", "disposition": "deferred", "reason": "SECURITY priority, clean merge: IDOR fix — slack_start_new_thread now enforces the deployment allowlist (_is_repo_allowed) and per-user repo access (require_repo_access_for_user keyed on the parent thread's github_login) before dispatching a run against a different repo. Both helpers exist in the fork at the same paths; merges clean. Port promptly.", "branch": "feature/upstream-clean-batch-jul21", "local_sha": null, "updated": "2026-07-21T18:00:23Z"}
|
||||
{"sha": "ab85b372", "pr": 1764, "subject": "fix: add exc_info to swallowed exception in push re-review webhook (#1764)", "disposition": "deferred", "reason": "Trivial: adds exc_info=True to the swallowed exception log in process_github_push_event (webhooks/github.py). Merges clean. Batch with the next clean-port round.", "branch": "feature/upstream-clean-batch-jul21", "local_sha": null, "updated": "2026-07-21T18:00:23Z"}
|
||||
{"sha": "7312851d", "pr": 1777, "subject": "feat: add explicit plan approval tool (#1777)", "disposition": "deferred", "reason": "Feature: explicit approve_plan tool + plan-mode exit. All deps exist in fork (plan_store PLAN_STATUS_APPROVED/SHARED, thread_api._user_owns_thread). Conflicts: prompt.py (fork prompt constants), server.py (tool/middleware wiring), check_message_queue.py (fork dashboard-handoff + mid-run injection drift), AGENTS.md. Hand re-key those four; keep the whole vertical (tool + plan_mode + thread_api + 4 test files) on one side.", "branch": "", "local_sha": null, "updated": "2026-07-21T18:00:33Z"}
|
||||
{"sha": "e51abe14", "pr": 1754, "subject": "fix: skip oversized images before model calls (#1754)", "disposition": "deferred", "reason": "Desirable: 10MB image size cap + 'image omitted' text block before model calls, prevents oversized-image model failures. Conflicts only because the fork hardened fetch_image_block (SSRF redirect guard, host-only logging) — hunks are compatible; re-key the size check around the fork's guarded fetch and port impl + test together.", "branch": "", "local_sha": null, "updated": "2026-07-21T18:00:33Z"}
|
||||
{"sha": "0ed560a5", "pr": 1779, "subject": "fix: settle review checks after run failures (#1779)", "disposition": "deferred", "reason": "Desirable: run-failure completion webhook now settles reviewer check runs left open when the graph dies (_settle_failed_reviewer_check; uses settle_review_check_run + review_check_pending_result, both already in the fork's review/publish.py from the #214/#215 ports). Conflict is fork drift in completion.py (failure-reply customizations); re-key the new helper in and port with its 159-line test.", "branch": "", "local_sha": null, "updated": "2026-07-21T18:00:33Z"}
|
||||
{"sha": "5b5e6076", "pr": 1790, "subject": "chore: Add open wiki docs (#1790)", "disposition": "wont-merge", "reason": "Upstream-repo openwiki doc site + AGENTS.md/CLAUDE.md pointers: content documents upstream's codebase and would be wrong for this fork (conflicts with the fork's heavily customized CLAUDE.md, which is canonical). Its companion auto-update workflow is bot-push docs automation counter to the fork's workflow-gating posture (#1773).", "branch": "", "local_sha": null, "updated": "2026-07-21T18:00:33Z"}
|
||||
{"sha": "1443fc4b", "pr": 1792, "subject": "fix: Update openwiki gh action (#1792)", "disposition": "wont-merge", "reason": "Follows #1790 (wont-merge): fixes the openwiki-update workflow this fork does not carry (modify/delete vs HEAD).", "branch": "", "local_sha": null, "updated": "2026-07-21T18:00:33Z"}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Commits on `upstream/main` (langchain-ai/open-swe) not yet in `dev`, and the dec
|
|||
Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred rows are provisional
|
||||
— re-inspect before picking. See the fork-maintenance runbook in `CLAUDE.md`.
|
||||
|
||||
**Last synced `upstream/main`:** `3ea29d3f` (2026-07-20)
|
||||
**Last synced `upstream/main`:** `1443fc4b` (2026-07-21)
|
||||
|
||||
| sha | pr | subject | decision | why | branch |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -85,6 +85,9 @@ Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred ro
|
|||
| `22383033` | #1758 | feat: inject extra JSON fields into sandbox create via env var (#1758) | Landed | Landed via fork PR #206, re-implemented against the fork's sync SandboxClient (upstream is async AsyncSandboxClient — retry/reconnect helpers not adopted; future picks touching them will conflict). | feature/port-upstream-clean-batch |
|
||||
| `e826864d` | #1760 | feat: optional separate LangSmith key/endpoint for sandboxes (#1760) | Landed | Landed via fork PR #206 on the sync client. Sandbox endpoint honors SANDBOX_LANGSMITH_ENDPOINT/LANGSMITH_ENDPOINT (LANGCHAIN_ENDPOINT alone no longer applies); new sandbox names thread-deterministic with _release_sandbox_name before create. | feature/port-upstream-clean-batch |
|
||||
| `dd5b7bec` | #1761 | fix: capitalize dashboard tool labels (#1761) | Landed | Landed via fork PR #206 (clean cherry-pick, stacked on #1732). | feature/port-upstream-clean-batch |
|
||||
| `b5e52925` | #1775 | feat: add structured Linear issue filters (#1775) | Landed | Clean pick: structured filters for linear_search_issues — stacks directly on #1748 (landed PR #206); zero fork drift on all three files. Ready whenever. | feature/upstream-clean-batch-security-linear |
|
||||
| `31263f83` | #1785 | Fix: Fix Stored XSS in ReplyCard.tsx (#1785) | Landed | SECURITY priority, near-clean: diff is urlencode() hardening of the GitHub OAuth authorize URL in dashboard routes.py auth_login (upstream bot title says ReplyCard.tsx — mismatch, trust the diff). Fork auth_login has the identical f-string URL; hunk applies clean. Port promptly. | feature/upstream-clean-batch-security-linear |
|
||||
| `3ea29d3f` | #1789 | Fix: Fix Improper privilege management in server.py (#1789) | Landed | SECURITY priority, near-clean: adds empty-signature reject to verify_github_signature (utils/github_comments.py) + verify_linear_signature (webhooks/common.py) (title says server.py — mismatch, trust the diff). Both fork functions match at the hunk sites; fork-only verify_jira_secret already guards empty token. Real value: None signature currently raises TypeError in compare_digest. Port BOTH hunks together. | feature/upstream-clean-batch-security-linear |
|
||||
| `c3292d82` | #1611 | bake sfw binary into sandbox image | Won't merge | already in dev | |
|
||||
| `48bf712b` | #1609 | show message timestamps | Won't merge | already in dev | |
|
||||
| `85c0f63e` | #1620 | clickable shared PR header | Won't merge | already in dev | |
|
||||
|
|
@ -124,6 +127,9 @@ Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred ro
|
|||
| `dccf6437` | #1769 | fix: tighten sandbox config test types (#1769) | Won't merge | test-only change in post-reorg path tests/sandbox/ — fork doesn't have this file (domain reorg #1726 deferred) | |
|
||||
| `c9a193e2` | #1766 | chore(deps): bump mcp from 1.27.2 to 1.28.1 (#1766) | Won't merge | indirect dependency bump (mcp); fork's own Dependabot handles these | |
|
||||
| `d0b63551` | #1773 | fix: remove workflow push approval gating (#1773) | Won't merge | Removes WorkflowPushGuardMiddleware and the proxy-token permission ladder — both actively wired in this fork (server.py middleware stack; github_app.py scoped-mint fallback). Adopting would let agent runs push .github/workflows/ changes with no human approval and mint proxy tokens at full scope unconditionally — a security-posture loosening counter to Sea Haven gating. Keep the fork's guard; skip the doc/prompt relaxation too (fork prompt documents the approval flow). | |
|
||||
| `589dfd83` | #1787 | fix: Harden Stagehand browser URL handling (#1787) | Won't merge | Follows #1648 (wont-merge): fork does not carry the Stagehand browser subagent — this 'fix' re-adds the entire module (992 insertions; modify/delete vs HEAD) plus the stagehand dep in pyproject/uv.lock. Adopting would resurrect a feature rejected under the curated-tools policy. | |
|
||||
| `5b5e6076` | #1790 | chore: Add open wiki docs (#1790) | Won't merge | Upstream-repo openwiki doc site + AGENTS.md/CLAUDE.md pointers: content documents upstream's codebase and would be wrong for this fork (conflicts with the fork's heavily customized CLAUDE.md, which is canonical). Its companion auto-update workflow is bot-push docs automation counter to the fork's workflow-gating posture (#1773). | |
|
||||
| `1443fc4b` | #1792 | fix: Update openwiki gh action (#1792) | Won't merge | Follows #1790 (wont-merge): fixes the openwiki-update workflow this fork does not carry (modify/delete vs HEAD). | |
|
||||
| `83abea26` | #1724 | fix: accept natural-language Slack plan approvals (#1724) | Deferred | natural-language Slack plan approvals; touches fork-diverged plan-mode + Slack webhook stack (#130); post-reorg test paths need remap | plan-approval |
|
||||
| `ddbe457b` | #1727 | fix: restore GPT-5.5 as default model (#1727) | Deferred | restores GPT-5.5 default in options/team_settings; fork picker is Bedrock/Fireworks-only — rides the #1708 OpenAI-models product decision (27b0ddeb) | model-picker |
|
||||
| `30832d29` | #1731 | fix: preserve OpenAI Responses tool history (#1731) | Deferred | deletes SanitizeOpenAIResponsesMiddleware in favor of replay-history preservation in utils/model.py; supersedes deferred #1718 (35659177) — triage the pair together against fork-diverged middleware + model.py | openai-sanitize |
|
||||
|
|
@ -135,11 +141,18 @@ Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred ro
|
|||
| `5077e2c7` | #1735 | feat(open-swe): untagged two-party Slack replies + debounced interrupts (#1735) | Deferred | untagged two-party Slack replies + debounced interrupts (~620 LOC incl. e2e); rides fork-diverged Slack webhook stack; own branch + e2e validation | slack-untagged-replies |
|
||||
| `8b26819f` | #1719 | fix: offload web tool results to sandbox (#1719) | Deferred | offloads large web tool results to sandbox files; touches fork-relevant web_search/http tools; check interplay with fork sandbox lifecycle | tool-offloading |
|
||||
| `b7c5dbd6` | #1747 | fix: simplify Slack run links (#1747) | Deferred | simplifies Slack run links; heavy churn on fork-diverged Slack context/prompt tests | slack-tooling |
|
||||
| `b5e52925` | #1775 | feat: add structured Linear issue filters (#1775) | Deferred | Clean pick: structured filters for linear_search_issues — stacks directly on #1748 (landed PR #206); zero fork drift on all three files. Ready whenever. | linear-tooling |
|
||||
| `81d544bc` | #1765 | feat: Expose more specific AGENTS.md context to Open SWE reviewer (#1765) | Deferred | Near-clean: agents_md.py helper + tests are zero-drift; reviewer.py hunk is small (~39 lines) but lands in the fork's heavily-diverged reviewer — hand-apply the scoped_agents_md wiring onto the fork's fetch_agents_md call sites (reviewer.py ~L404/445/1031). | reviewer-context |
|
||||
| `8c8e58bc` | #1776 | feat: connect automations to Slack channels (#1776) | Deferred | Moderate reconcile: Slack-channel wiring for automations. Fork helpers exist (post_slack_top_level_message_with_ts, generate_thread_id_from_slack_thread, slack_id_for_login); completion.py (+233 drift) and schedules.py (+167) need hand-merge; UI automations feature present. Keep backend+UI+tests as one vertical. | automations-slack |
|
||||
| `f0897479` | #1778 | feat: surface context window usage in agents UI (#1778) | Deferred | Near-clean: context-window indicator UI is all new files (zero drift); options.py hunk must be re-keyed to the fork's Bedrock/Fireworks model map (fork model IDs differ from upstream's) — add context_window per fork entry rather than taking upstream values. | context-usage-ui |
|
||||
| `31263f83` | #1785 | Fix: Fix Stored XSS in ReplyCard.tsx (#1785) | Deferred | SECURITY priority, near-clean: diff is urlencode() hardening of the GitHub OAuth authorize URL in dashboard routes.py auth_login (upstream bot title says ReplyCard.tsx — mismatch, trust the diff). Fork auth_login has the identical f-string URL; hunk applies clean. Port promptly. | sec-oauth-urlencode |
|
||||
| `3ea29d3f` | #1789 | Fix: Fix Improper privilege management in server.py (#1789) | Deferred | SECURITY priority, near-clean: adds empty-signature reject to verify_github_signature (utils/github_comments.py) + verify_linear_signature (webhooks/common.py) (title says server.py — mismatch, trust the diff). Both fork functions match at the hunk sites; fork-only verify_jira_secret already guards empty token. Real value: None signature currently raises TypeError in compare_digest. Port BOTH hunks together. | sec-webhook-empty-sig |
|
||||
| `2e8ff4b7` | #1782 | chore: clarify shared response image guidance (#1782) | Deferred | Near-clean: docstring-only guidance in save_plan (shared responses persist Markdown only, never sandbox-local images — post screenshots directly to Slack) + 2 assertions in test_plan_review. Merges clean against fork. Batch with the next clean-port round. | feature/upstream-clean-batch-jul21 |
|
||||
| `4ea2441a` | #1791 | fix: match embedded review description background (#1791) | Deferred | Near-clean UI-only: ReviewMainBody.tsx background match for the embedded review description; merges clean, zero fork drift. Batch with the next clean-port round. | feature/upstream-clean-batch-jul21 |
|
||||
| `9bbd65d3` | #1781 | fix: derive model context windows from LangChain profiles (#1781) | Deferred | Pair with deferred #1778 on context-usage-ui: derives model context windows from LangChain profiles. options.py conflicts — re-key to the fork's Bedrock/Fireworks model map (upstream IDs differ; verify LangChain profiles even resolve for the fork's Bedrock-style IDs, else keep static values). uv.lock bumps langchain to a profiles-capable version; test_model_fallback_resolution also conflicts. Port with/after #1778. | context-usage-ui |
|
||||
| `df743658` | #1774 | feat: add repository skill support to the coding agent (#1774) | Deferred | New feature: loads repo-provided skills into the coding agent (TrustedSkillsMiddleware, a deepagents SkillsMiddleware subclass pinned to a trusted ref, + repo_prep discovery). SECURITY review required before adopting — skills are repo-controlled prompt content (injection surface); verify the trusted-ref pinning against the fork threat model before wiring. Only server.py conflicts (fork tool/middleware wiring — re-key stack order); trusted_skills.py and repo_prep.py hunks merge clean. | |
|
||||
| `75fb8b48` | #1786 | Fix PR creation guard shell bypasses (#1786) | Deferred | SECURITY priority, clean merge: closes shell-bypass holes in PullRequestCreationGuardMiddleware (nested 'bash -c' expansion to depth 3, quoted-executable normalization) — a guard this fork actively wires. Port promptly; ALSO mirror the nested-shell expansion into the fork-only PullRequestVerdictGuardMiddleware (pr_verdict_guard.py), which shares the naive shlex approach and has the same bypass shape. | feature/upstream-clean-batch-jul21 |
|
||||
| `32e81f29` | #1788 | Fix: Fix Insecure Direct Object Reference in slack_start_new_thread.py (#1788) | Deferred | SECURITY priority, clean merge: IDOR fix — slack_start_new_thread now enforces the deployment allowlist (_is_repo_allowed) and per-user repo access (require_repo_access_for_user keyed on the parent thread's github_login) before dispatching a run against a different repo. Both helpers exist in the fork at the same paths; merges clean. Port promptly. | feature/upstream-clean-batch-jul21 |
|
||||
| `ab85b372` | #1764 | fix: add exc_info to swallowed exception in push re-review webhook (#1764) | Deferred | Trivial: adds exc_info=True to the swallowed exception log in process_github_push_event (webhooks/github.py). Merges clean. Batch with the next clean-port round. | feature/upstream-clean-batch-jul21 |
|
||||
| `7312851d` | #1777 | feat: add explicit plan approval tool (#1777) | Deferred | Feature: explicit approve_plan tool + plan-mode exit. All deps exist in fork (plan_store PLAN_STATUS_APPROVED/SHARED, thread_api._user_owns_thread). Conflicts: prompt.py (fork prompt constants), server.py (tool/middleware wiring), check_message_queue.py (fork dashboard-handoff + mid-run injection drift), AGENTS.md. Hand re-key those four; keep the whole vertical (tool + plan_mode + thread_api + 4 test files) on one side. | |
|
||||
| `e51abe14` | #1754 | fix: skip oversized images before model calls (#1754) | Deferred | Desirable: 10MB image size cap + 'image omitted' text block before model calls, prevents oversized-image model failures. Conflicts only because the fork hardened fetch_image_block (SSRF redirect guard, host-only logging) — hunks are compatible; re-key the size check around the fork's guarded fetch and port impl + test together. | |
|
||||
| `0ed560a5` | #1779 | fix: settle review checks after run failures (#1779) | Deferred | Desirable: run-failure completion webhook now settles reviewer check runs left open when the graph dies (_settle_failed_reviewer_check; uses settle_review_check_run + review_check_pending_result, both already in the fork's review/publish.py from the #214/#215 ports). Conflict is fork drift in completion.py (failure-reply customizations); re-key the new helper in and port with its 159-line test. | |
|
||||
|
||||
_Maintenance: after a `git sync`, add new `dev..upstream/main` SHAs as **Untriaged** (edit `triage.jsonl`) and bump "Last synced". A successful `git cherry-pick -x` auto-moves the row to **Landed** via the `post-commit` journal + `make triage-reconcile`._
|
||||
|
|
|
|||
|
|
@ -2429,7 +2429,7 @@ async def test_publish_review_drops_verdict_when_not_requested() -> None:
|
|||
patch("agent.tools.publish_review.get_github_token", return_value="token"),
|
||||
patch("agent.tools.publish_review._publish_review_async", publish_async),
|
||||
):
|
||||
result = await publish_review(verdict="approve")
|
||||
result = await publish_review(verdict="request_changes")
|
||||
|
||||
assert publish_async.call_args.kwargs["verdict"] is None
|
||||
assert result["verdict_ignored"] is True
|
||||
|
|
@ -2437,6 +2437,38 @@ async def test_publish_review_drops_verdict_when_not_requested() -> None:
|
|||
assert result["verdict_submitted"] is False
|
||||
|
||||
|
||||
async def test_publish_review_forwards_unsolicited_approve() -> None:
|
||||
"""An approve without verdict_requested is forwarded (not dropped) with the
|
||||
unsolicited flag set, so the async layer can apply the clean-review gate."""
|
||||
from agent.tools.publish_review import publish_review
|
||||
|
||||
publish_async = AsyncMock(
|
||||
return_value={"success": True, "review_id": 9, "verdict_submitted": True}
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"agent.tools.publish_review.get_config",
|
||||
return_value={
|
||||
"configurable": {
|
||||
"thread_id": "tid",
|
||||
"repo": {"owner": "o", "name": "r"},
|
||||
"pr_number": 7,
|
||||
"head_sha": "sha",
|
||||
},
|
||||
"metadata": {},
|
||||
},
|
||||
),
|
||||
patch("agent.tools.publish_review.get_github_token", return_value="token"),
|
||||
patch("agent.tools.publish_review._publish_review_async", publish_async),
|
||||
):
|
||||
result = await publish_review(verdict="approve")
|
||||
|
||||
assert publish_async.call_args.kwargs["verdict"] == "approve"
|
||||
assert publish_async.call_args.kwargs["unsolicited_approve"] is True
|
||||
assert result["verdict_submitted"] is True
|
||||
assert "verdict_ignored" not in result
|
||||
|
||||
|
||||
async def test_publish_review_forwards_authorized_verdict() -> None:
|
||||
from agent.tools.publish_review import publish_review
|
||||
|
||||
|
|
@ -2542,6 +2574,103 @@ async def test_publish_async_approve_with_zero_findings_bypasses_empty_skip() ->
|
|||
assert "Verdict (`approve`) submitted at the request of @amoussa1229." in body
|
||||
|
||||
|
||||
async def test_publish_async_unsolicited_approve_clean_posts_approve() -> None:
|
||||
"""A clean review (zero open findings) lands an unsolicited approve as a
|
||||
real APPROVE, with automatic (not requester) attribution."""
|
||||
from contextlib import ExitStack
|
||||
|
||||
from agent.tools.publish_review import _publish_review_async
|
||||
|
||||
post_review = AsyncMock(return_value={"id": 2001})
|
||||
with ExitStack() as stack:
|
||||
for p in _verdict_publish_patches(findings=[], post_review=post_review):
|
||||
stack.enter_context(p)
|
||||
result = await _publish_review_async(
|
||||
owner="o",
|
||||
repo="r",
|
||||
pr_number=7,
|
||||
head_sha="sha",
|
||||
token="t",
|
||||
severity_threshold="medium",
|
||||
cap=15,
|
||||
is_re_review=False,
|
||||
verdict="approve",
|
||||
verdict_requester="",
|
||||
unsolicited_approve=True,
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["verdict_submitted"] is True
|
||||
assert result["verdict_event"] == "APPROVE"
|
||||
assert post_review.await_args.kwargs["event"] == "APPROVE"
|
||||
body = post_review.await_args.kwargs["body"]
|
||||
assert "Verdict (`approve`) submitted — the review found no open issues." in body
|
||||
assert "at the request of" not in body
|
||||
|
||||
|
||||
async def test_publish_async_unsolicited_approve_with_open_findings_downgrades() -> None:
|
||||
"""An unsolicited approve alongside open findings is downgraded to a
|
||||
comment review — approving while requesting changes is contradictory."""
|
||||
from contextlib import ExitStack
|
||||
|
||||
from agent.tools.publish_review import _publish_review_async
|
||||
|
||||
findings = [_f(id="f_1", severity="high", file="a.py", start_line=1, end_line=1)]
|
||||
post_review = AsyncMock(return_value={"id": 2002})
|
||||
with ExitStack() as stack:
|
||||
for p in _verdict_publish_patches(findings=findings, post_review=post_review):
|
||||
stack.enter_context(p)
|
||||
result = await _publish_review_async(
|
||||
owner="o",
|
||||
repo="r",
|
||||
pr_number=7,
|
||||
head_sha="sha",
|
||||
token="t",
|
||||
severity_threshold="medium",
|
||||
cap=15,
|
||||
is_re_review=False,
|
||||
verdict="approve",
|
||||
verdict_requester="",
|
||||
unsolicited_approve=True,
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result.get("verdict_submitted") is not True
|
||||
assert result["verdict_ignored"] is True
|
||||
assert result["verdict_ignored_reason"] == "approve_with_open_findings"
|
||||
assert post_review.await_args.kwargs["event"] == "COMMENT"
|
||||
|
||||
|
||||
async def test_publish_async_authorized_approve_ignores_open_findings_gate() -> None:
|
||||
"""The explicit-request path is unchanged: an authorized approve is honored
|
||||
even when open findings exist (the requester asked for the verdict)."""
|
||||
from contextlib import ExitStack
|
||||
|
||||
from agent.tools.publish_review import _publish_review_async
|
||||
|
||||
findings = [_f(id="f_1", severity="high", file="a.py", start_line=1, end_line=1)]
|
||||
post_review = AsyncMock(return_value={"id": 2003})
|
||||
with ExitStack() as stack:
|
||||
for p in _verdict_publish_patches(findings=findings, post_review=post_review):
|
||||
stack.enter_context(p)
|
||||
result = await _publish_review_async(
|
||||
owner="o",
|
||||
repo="r",
|
||||
pr_number=7,
|
||||
head_sha="sha",
|
||||
token="t",
|
||||
severity_threshold="medium",
|
||||
cap=15,
|
||||
is_re_review=False,
|
||||
verdict="approve",
|
||||
verdict_requester="amoussa1229",
|
||||
)
|
||||
|
||||
assert result["verdict_submitted"] is True
|
||||
assert result["verdict_event"] == "APPROVE"
|
||||
assert post_review.await_args.kwargs["event"] == "APPROVE"
|
||||
|
||||
|
||||
async def test_publish_async_request_changes_maps_event() -> None:
|
||||
from contextlib import ExitStack
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,59 @@ async def test_search_issues_returns_results_and_pagination(
|
|||
}
|
||||
|
||||
|
||||
async def test_search_issues_rejects_blank_query(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def test_search_issues_filters_without_text(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
async def fake_graphql_request(
|
||||
query: str, variables: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
captured.update({"query": query, "variables": variables})
|
||||
return {
|
||||
"issues": {
|
||||
"nodes": [{"id": "issue-id", "identifier": "DCD-21", "title": "Fix filters"}],
|
||||
"totalCount": 1,
|
||||
"pageInfo": {"hasNextPage": False, "endCursor": None},
|
||||
}
|
||||
}
|
||||
|
||||
monkeypatch.setattr(linear, "_graphql_request", fake_graphql_request)
|
||||
filters = {"labels": {"some": {"name": {"eq": "open-swe"}}}}
|
||||
|
||||
result = await linear.search_issues(filters=filters, limit=1)
|
||||
|
||||
assert "issues(" in captured["query"]
|
||||
assert "searchIssues" not in captured["query"]
|
||||
assert captured["variables"] == {
|
||||
"filter": filters,
|
||||
"limit": 1,
|
||||
"includeArchived": False,
|
||||
"after": None,
|
||||
}
|
||||
assert result["issues"][0]["identifier"] == "DCD-21"
|
||||
|
||||
|
||||
async def test_search_issues_combines_filters_with_team(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
async def fake_graphql_request(
|
||||
_query: str, variables: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
captured["variables"] = variables
|
||||
return {"searchIssues": {"nodes": [], "totalCount": 0, "pageInfo": {}}}
|
||||
|
||||
monkeypatch.setattr(linear, "_graphql_request", fake_graphql_request)
|
||||
filters = {"state": {"name": {"eq": "Todo"}}}
|
||||
|
||||
await linear.search_issues("fix", team_id="team-id", filters=filters)
|
||||
|
||||
assert captured["variables"]["filter"] == {
|
||||
"and": [filters, {"team": {"id": {"eq": "team-id"}}}]
|
||||
}
|
||||
|
||||
|
||||
async def test_search_issues_rejects_missing_query_and_filters(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def unexpected_request(*_args: Any, **_kwargs: Any) -> dict[str, Any]:
|
||||
pytest.fail("GraphQL request should not be made")
|
||||
|
||||
|
|
@ -74,7 +126,7 @@ async def test_search_issues_rejects_blank_query(monkeypatch: pytest.MonkeyPatch
|
|||
|
||||
result = await linear.search_issues(" ")
|
||||
|
||||
assert result == {"error": "Search query must not be empty"}
|
||||
assert result == {"error": "Search query or filters must be provided"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("limit", [0, 51])
|
||||
|
|
@ -114,6 +166,7 @@ async def test_linear_search_issues_tool_delegates(monkeypatch: pytest.MonkeyPat
|
|||
result = await linear_search_tool.linear_search_issues(
|
||||
"styling",
|
||||
team_id="team-id",
|
||||
filters={"priority": {"eq": 1}},
|
||||
limit=20,
|
||||
include_archived=True,
|
||||
include_comments=True,
|
||||
|
|
@ -124,6 +177,7 @@ async def test_linear_search_issues_tool_delegates(monkeypatch: pytest.MonkeyPat
|
|||
assert captured == {
|
||||
"query": "styling",
|
||||
"team_id": "team-id",
|
||||
"filters": {"priority": {"eq": 1}},
|
||||
"limit": 20,
|
||||
"include_archived": True,
|
||||
"include_comments": True,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue