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 logging
import os import os
from typing import Any, Literal from typing import Any, Literal
from urllib.parse import urlencode
import httpx import httpx
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request
@ -377,12 +378,14 @@ async def auth_login(
nonce_hash=hash_state_nonce(nonce), nonce_hash=hash_state_nonce(nonce),
) )
redirect_uri = f"{_api_base_url()}/dashboard/api/auth/callback" redirect_uri = f"{_api_base_url()}/dashboard/api/auth/callback"
url = ( query = urlencode(
"https://github.com/login/oauth/authorize" {
f"?client_id={client_id}" "client_id": client_id,
f"&redirect_uri={redirect_uri}" "redirect_uri": redirect_uri,
f"&state={state}" "state": state,
}
) )
url = f"https://github.com/login/oauth/authorize?{query}"
response = RedirectResponse(url, status_code=302) response = RedirectResponse(url, status_code=302)
_set_state_cookie(response, nonce) _set_state_cookie(response, nonce)
return response return response

View file

@ -4,21 +4,23 @@ from ..utils.linear import search_issues
async def linear_search_issues( async def linear_search_issues(
query: str, query: str | None = None,
team_id: str | None = None, team_id: str | None = None,
filters: dict[str, Any] | None = None,
limit: int = 10, limit: int = 10,
include_archived: bool = False, include_archived: bool = False,
include_comments: bool = False, include_comments: bool = False,
after: str | None = None, after: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Search Linear issues by title, description, and optionally comments. """Search Linear issues by text, structured filters, or both.
Args: 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. 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. limit: Maximum results to return, from 1 to 50.
include_archived: Whether to include archived issues. 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. after: Optional pagination cursor from a previous result's page_info.endCursor.
Returns: Returns:
@ -27,6 +29,7 @@ async def linear_search_issues(
return await search_issues( return await search_issues(
query=query, query=query,
team_id=team_id, team_id=team_id,
filters=filters,
limit=limit, limit=limit,
include_archived=include_archived, include_archived=include_archived,
include_comments=include_comments, 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") logger.warning("GITHUB_WEBHOOK_SECRET is not configured — rejecting webhook request")
return False return False
if not signature:
return False
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature) 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( async def search_issues(
query: str, query: str | None = None,
team_id: str | None = None, team_id: str | None = None,
filters: dict[str, Any] | None = None,
limit: int = 10, limit: int = 10,
include_archived: bool = False, include_archived: bool = False,
include_comments: bool = False, include_comments: bool = False,
after: str | None = None, after: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Search Linear issues by free-text query.""" """Search Linear issues by text, structured filters, or both."""
query = query.strip() query = (query or "").strip()
if not query: issue_filter = dict(filters or {})
return {"error": "Search query must not be empty"} 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: if not 1 <= limit <= 50:
return {"error": "Search limit must be between 1 and 50"} return {"error": "Search limit must be between 1 and 50"}
search_query = """ connection_fields = """
query SearchIssues( totalCount
$query: String! pageInfo {
$filter: IssueFilter hasNextPage
$limit: Int! endCursor
$includeArchived: Boolean }
$includeComments: Boolean nodes {
$after: String id
) { identifier
searchIssues( title
term: $query priority
filter: $filter priorityLabel
first: $limit state { id name type }
includeArchived: $includeArchived assignee { id name email }
includeComments: $includeComments team { id name key }
after: $after project { id name }
) { labels { nodes { id name } }
totalCount createdAt
pageInfo { updatedAt
hasNextPage archivedAt
endCursor url
}
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( if query:
search_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, "query": query,
"filter": {"team": {"id": {"eq": team_id}}} if team_id else None, "filter": issue_filter or None,
"limit": limit, "limit": limit,
"includeArchived": include_archived, "includeArchived": include_archived,
"includeComments": include_comments, "includeComments": include_comments,
"after": after, "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: if "error" in result:
return result return result
search_results = result.get("searchIssues", {}) search_results = result.get(connection_name, {})
return { return {
"issues": search_results.get("nodes", []), "issues": search_results.get("nodes", []),
"total_count": search_results.get("totalCount", 0), "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") logger.warning("LINEAR_WEBHOOK_SECRET is not configured — rejecting webhook request")
return False return False
if not signature:
return False
expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest() expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature): if not hmac.compare_digest(expected, signature):
return False 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": "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": "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": "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": "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": "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": "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": "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": "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": "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 | | `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 | | `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 | | `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 | | | `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 | | | `48bf712b` | #1609 | show message timestamps | Won't merge | already in dev | |
| `85c0f63e` | #1620 | clickable shared PR header | 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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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`._ _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]: async def unexpected_request(*_args: Any, **_kwargs: Any) -> dict[str, Any]:
pytest.fail("GraphQL request should not be made") 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(" ") 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]) @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( result = await linear_search_tool.linear_search_issues(
"styling", "styling",
team_id="team-id", team_id="team-id",
filters={"priority": {"eq": 1}},
limit=20, limit=20,
include_archived=True, include_archived=True,
include_comments=True, include_comments=True,
@ -124,6 +177,7 @@ async def test_linear_search_issues_tool_delegates(monkeypatch: pytest.MonkeyPat
assert captured == { assert captured == {
"query": "styling", "query": "styling",
"team_id": "team-id", "team_id": "team-id",
"filters": {"priority": {"eq": 1}},
"limit": 20, "limit": 20,
"include_archived": True, "include_archived": True,
"include_comments": True, "include_comments": True,