mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 11:22:10 +00:00
fix: resolve dashboard run email from GitHub mapping, not OAuth profile (#1372)
When sending a message to a thread from the Agents chat UI, the run used
the OAuth profile email (which can be a personal account that isn't an org
member). For Slack-originated threads resumed from the UI, auth routes
through the email-based path and fails ("Could not find a LangSmith account").
Resolve the run email via the GitHub->email mapping first, falling back to
the profile email only when no mapping exists, matching the github-source
auth path.
This commit is contained in:
parent
c41185a3ca
commit
7a9785c9d3
2 changed files with 70 additions and 2 deletions
|
|
@ -21,6 +21,7 @@ from .message_adapter import state_messages_to_ui
|
||||||
from .options import SUPPORTED_MODEL_IDS, model_supports_effort
|
from .options import SUPPORTED_MODEL_IDS, model_supports_effort
|
||||||
from .profiles import OAUTH_TOKENS_NAMESPACE, get_profile, get_valid_access_token
|
from .profiles import OAUTH_TOKENS_NAMESPACE, get_profile, get_valid_access_token
|
||||||
from .profiles import _get_value as get_oauth_record
|
from .profiles import _get_value as get_oauth_record
|
||||||
|
from .user_mappings import email_for_login
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -36,6 +37,17 @@ def _agent_version_metadata() -> dict[str, str]:
|
||||||
return {"LANGSMITH_AGENT_VERSION": revision} if revision else {}
|
return {"LANGSMITH_AGENT_VERSION": revision} if revision else {}
|
||||||
|
|
||||||
|
|
||||||
|
async def _resolve_run_email(login: str, profile: dict[str, Any]) -> str | None:
|
||||||
|
"""Email used for GitHub/LangSmith auth on a run.
|
||||||
|
|
||||||
|
Prefers the admin/self GitHub→email mapping (the work email known to
|
||||||
|
the org) over the OAuth profile email, which may be a personal account
|
||||||
|
that isn't an org member.
|
||||||
|
"""
|
||||||
|
mapped = await email_for_login(login)
|
||||||
|
return mapped or profile.get("email")
|
||||||
|
|
||||||
|
|
||||||
class ThreadCreateBody(BaseModel):
|
class ThreadCreateBody(BaseModel):
|
||||||
prompt: str = Field(min_length=1, max_length=20_000)
|
prompt: str = Field(min_length=1, max_length=20_000)
|
||||||
repo: str | None = None
|
repo: str | None = None
|
||||||
|
|
@ -320,7 +332,7 @@ async def _start_agent_run(
|
||||||
"source": _DASHBOARD_SOURCE,
|
"source": _DASHBOARD_SOURCE,
|
||||||
"github_login": login,
|
"github_login": login,
|
||||||
"repo": repo_config,
|
"repo": repo_config,
|
||||||
"user_email": profile.get("email"),
|
"user_email": await _resolve_run_email(login, profile),
|
||||||
}
|
}
|
||||||
if chosen_model and chosen_effort:
|
if chosen_model and chosen_effort:
|
||||||
configurable["agent_model_id"] = chosen_model
|
configurable["agent_model_id"] = chosen_model
|
||||||
|
|
@ -400,7 +412,7 @@ async def send_dashboard_message(
|
||||||
"source": thread_source,
|
"source": thread_source,
|
||||||
"github_login": login,
|
"github_login": login,
|
||||||
"repo": {"owner": owner, "name": name},
|
"repo": {"owner": owner, "name": name},
|
||||||
"user_email": profile.get("email"),
|
"user_email": await _resolve_run_email(login, profile),
|
||||||
}
|
}
|
||||||
source_context = metadata.get("source_context")
|
source_context = metadata.get("source_context")
|
||||||
if isinstance(source_context, dict):
|
if isinstance(source_context, dict):
|
||||||
|
|
|
||||||
56
tests/test_dashboard_run_email.py
Normal file
56
tests/test_dashboard_run_email.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from agent.dashboard import thread_api
|
||||||
|
from agent.dashboard import user_mappings as um
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeStore:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.items: dict[tuple[tuple[str, ...], str], dict[str, Any]] = {}
|
||||||
|
|
||||||
|
async def get_item(self, namespace: list[str], key: str):
|
||||||
|
value = self.items.get((tuple(namespace), key))
|
||||||
|
return {"value": value} if value is not None else None
|
||||||
|
|
||||||
|
async def put_item(self, namespace: list[str], key: str, value: dict[str, Any]) -> None:
|
||||||
|
self.items[(tuple(namespace), key)] = value
|
||||||
|
|
||||||
|
async def search_items(self, namespace: list[str], *, limit: int = 1000):
|
||||||
|
ns = tuple(namespace)
|
||||||
|
items = [{"value": v} for (n, _k), v in self.items.items() if n == ns]
|
||||||
|
return {"items": items[:limit]}
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeClient:
|
||||||
|
def __init__(self, store: _FakeStore) -> None:
|
||||||
|
self.store = store
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def fake_store(monkeypatch: pytest.MonkeyPatch) -> _FakeStore:
|
||||||
|
store = _FakeStore()
|
||||||
|
monkeypatch.setattr(um, "_client", lambda: _FakeClient(store))
|
||||||
|
um.clear_cache()
|
||||||
|
return store
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_email_prefers_github_mapping(fake_store: _FakeStore) -> None:
|
||||||
|
await um.upsert_mapping(
|
||||||
|
github_login="johannes117",
|
||||||
|
work_email="johannes@langchain.dev",
|
||||||
|
source="admin",
|
||||||
|
)
|
||||||
|
# OAuth profile carries a personal account that isn't an org member.
|
||||||
|
profile = {"email": "johannesduplessis117@gmail.com"}
|
||||||
|
assert await thread_api._resolve_run_email("johannes117", profile) == "johannes@langchain.dev"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_email_falls_back_to_profile_when_unmapped(fake_store: _FakeStore) -> None:
|
||||||
|
profile = {"email": "someone@example.com"}
|
||||||
|
assert await thread_api._resolve_run_email("nomapping", profile) == "someone@example.com"
|
||||||
Loading…
Add table
Reference in a new issue