fix(open-swe): restrict Linear token attribution to comment author only

Split actor_email (comment_author only, feeds github_login for token
attribution) from user_email (full fallback chain, for display/model).
This ensures a PR is never opened as a non-actor (creator/assignee).

Add test_resolve_github_token_linear_defaults_to_bot to lock the
security-critical default: Linear + mapped login + no opt-in → bot.
This commit is contained in:
amoussa1229 2026-07-13 19:01:36 +00:00
parent b8f08a4d7b
commit d201e361a9
2 changed files with 33 additions and 6 deletions

View file

@ -40,12 +40,19 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
if not full_issue:
full_issue = issue_data
user_email = None
user_name = None
# Actor email for token attribution: restricted to the comment author only.
# The creator/assignee fallback chain is intentionally excluded here so a PR
# is never opened as a non-actor.
actor_email = None
comment_author = issue_data.get("comment_author", {})
if comment_author:
user_email = comment_author.get("email")
actor_email = comment_author.get("email")
user_name = comment_author.get("name")
# User email with full fallback chain for display, model selection, and
# @mention instructions.
user_email = actor_email
if not user_email:
creator = full_issue.get("creator", {})
if creator:
@ -165,10 +172,11 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
)
content_blocks: list[dict[str, Any]] = [create_text_block(prompt)]
# Resolve the GitHub login from the Linear email via the same user-mapping
# store Slack uses, so PRs open *as the triggering user* and the thread is
# tagged for the dashboard.
mapped_login = await webapp.resolve_login_from_email_async(user_email) if user_email else None
# Resolve the GitHub login from the actor's Linear email via the same
# user-mapping store Slack uses, so PRs open *as the triggering user* and the
# thread is tagged for the dashboard. Restricted to the comment author so
# token attribution never falls back to creator/assignee.
mapped_login = await webapp.resolve_login_from_email_async(actor_email) if actor_email else None
image_model_override: tuple[str, str] | None = None
if image_urls:

View file

@ -234,6 +234,25 @@ def test_resolve_github_token_linear_optin_no_token_falls_back_to_bot_in_bot_onl
assert (token, expires_at) == ("bot-tok", None)
def test_resolve_github_token_linear_defaults_to_bot(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# DEFAULT (no author_prs_as_user opt-in): linear runs author as the app bot,
# even when a valid per-user token and mapped login exist — so PRs can never
# be opened as a non-actor (creator/assignee).
_stub_dashboard_store(monkeypatch, token="user-tok")
_set_profile(monkeypatch, author_prs_as_user=False)
monkeypatch.setattr(auth, "is_bot_token_only_mode", lambda: False)
async def fake_bot(thread_id: str):
return ("bot-tok", None)
monkeypatch.setattr(auth, "_resolve_bot_installation_token", fake_bot)
token, _ = asyncio.run(auth.resolve_github_token(_linear_config(), "t1"))
assert token == "bot-tok"
@pytest.mark.parametrize("source", ["github"])
def test_resolve_github_token_bot_only_mode_non_slack_uses_bot(
monkeypatch: pytest.MonkeyPatch, source: str