feat(open-swe): port upstream clean batch (#1775, #1785, #1789) (#215)
Some checks are pending
CI / Lint (push) Waiting to run
CI / Format check (push) Waiting to run
CI / Typecheck (push) Waiting to run
CI / Unit tests (push) Waiting to run
CI / Playwright E2E (push) Waiting to run
CI / Docker build smoke (push) Waiting to run
CI / Triage ledger up to date (push) Waiting to run
CI / ui bun.lock in sync (push) Waiting to run

* Fix: Fix Improper privilege management in server.py (#1789)

Co-authored-by: corridor-security[bot] <203152403+corridor-security[bot]@users.noreply.github.com>
(cherry picked from commit 3ea29d3f231dd66bd7627769b5659564be4525df)

* Fix: Fix Stored XSS in ReplyCard.tsx (#1785)

Co-authored-by: corridor-security[bot] <203152403+corridor-security[bot]@users.noreply.github.com>
(cherry picked from commit 31263f832a2ecedf669eee2e27b827a358a6a4d7)

* feat: add structured Linear issue filters (#1775)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
(cherry picked from commit b5e529252c3012818289eca1b1cdeb8014721310)

* chore(triage): mark #1775 #1785 #1789 landed

Move the three clean cherry-picks in this batch from deferred to landed
in the upstream-sync ledger and re-render triage.md.

---------

Co-authored-by: corridor-security[bot] <203152403+corridor-security[bot]@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Adam Moussa 2026-07-20 15:54:16 -04:00 • committed by GitHub
parent 0f0f616cd4
commit db05d5826f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 168 additions and 68 deletions

View file

@ -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

View file

@ -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,

View file

@ -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)

View file

@ -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),

View file

@ -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

View file

@ -124,9 +124,9 @@
{"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"}

View file

@ -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 | |
@ -135,11 +138,8 @@ 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 |
_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`._

View file

@ -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,