Skip to content

Commit b62f69f

Browse files
azulusclaude
andauthored
feat(seer): Thread short-lived API token to Explorer MCP tools (#112179)
Create a short-lived (1-hour), org-scoped API token when the user opens the Seer Explorer and pass it to Seer via `user_auth_token` on the `ExplorerChatRequest`. This enables the new MCP `sentry_api_execute` tool to call the Sentry API on behalf of the user. ## What - Create `ApiToken` in `collect_user_org_context()` with scopes: `org:read/write`, `project:read/write`, `event:read/write`, `alerts:read/write`, `member:read`, `team:read` - Resolve `RpcUser` → real `User` model for token creation - Pass `request=request` through to `start_run()` so the token can be created with the authenticated user - Add `user_auth_token` field to `ExplorerChatRequest` TypedDict - Add instrumentation logging at each stage ## Security - Token scoped to single org via `scoping_organization_id` - 1-hour expiry - Created per-session, not reused ## Companion PR Seer-side changes (tool registration + threading): getsentry/seer#5626 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d9ac5bc commit b62f69f

File tree

5 files changed

+80
-6
lines changed

5 files changed

+80
-6
lines changed

src/sentry/features/temporary.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
317317
manager.add("organizations:seer-explorer-context-engine-fe-override-ui-flag", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
318318
# Enable code editing tools in Seer Explorer chat
319319
manager.add("organizations:seer-explorer-chat-coding", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
320+
# Enable code mode tools (sentry_api_search/execute) in Seer Explorer
321+
manager.add("organizations:seer-explorer-code-mode-tools", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
320322
# Enable structured LLM context (JSON snapshot) instead of ASCII DOM snapshot
321323
manager.add("organizations:context-engine-structured-page-context", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
322324
# Enable the Seer Overview sections for Seat-Based users

src/sentry/seer/endpoints/organization_seer_explorer_chat.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,15 @@ def post(
172172
) and features.has(
173173
"organizations:seer-explorer-chat-coding", organization, actor=request.user
174174
)
175+
enable_code_mode_tools = features.has(
176+
"organizations:seer-explorer-code-mode-tools", organization, actor=request.user
177+
)
175178
client = SeerExplorerClient(
176179
organization,
177180
request.user,
178181
is_interactive=True,
179182
enable_coding=enable_coding,
183+
enable_code_mode_tools=enable_code_mode_tools,
180184
)
181185
if run_id:
182186
# Continue existing conversation
@@ -186,6 +190,7 @@ def post(
186190
insert_index=insert_index,
187191
on_page_context=on_page_context,
188192
page_name=page_name,
193+
request=request,
189194
)
190195
else:
191196
# Start new conversation
@@ -194,6 +199,7 @@ def post(
194199
on_page_context=on_page_context,
195200
page_name=page_name,
196201
override_ce_enable=override_ce_enable,
202+
request=request,
197203
)
198204

199205
return Response({"run_id": result_run_id})

src/sentry/seer/explorer/client.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
ExplorerRunsRequest,
2121
ExplorerUpdateRequest,
2222
collect_user_org_context,
23+
create_explorer_api_token,
2324
fetch_run_status,
2425
make_explorer_chat_request,
2526
make_explorer_runs_request,
@@ -196,6 +197,7 @@ def __init__(
196197
intelligence_level: Literal["low", "medium", "high"] = "medium",
197198
is_interactive: bool = False,
198199
enable_coding: bool = False,
200+
enable_code_mode_tools: bool = False,
199201
max_iterations: int | None = None,
200202
):
201203
self.organization = organization
@@ -207,6 +209,7 @@ def __init__(
207209
self.category_key = category_key
208210
self.category_value = category_value
209211
self.is_interactive = is_interactive
212+
self.enable_code_mode_tools = enable_code_mode_tools
210213
self.max_iterations = max_iterations
211214

212215
if enable_coding and not organization.get_option("sentry:enable_seer_coding", True):
@@ -266,19 +269,28 @@ def start_run(
266269
if bool(artifact_schema) != bool(artifact_key):
267270
raise ValueError("artifact_key and artifact_schema must be provided together")
268271

272+
user_org_context = collect_user_org_context(self.user, self.organization, request=request)
273+
user_auth_token = (
274+
create_explorer_api_token(self.user, self.organization)
275+
if self.enable_code_mode_tools
276+
and self.user
277+
and not isinstance(self.user, AnonymousUser)
278+
else None
279+
)
280+
269281
chat_body: ExplorerChatRequest = ExplorerChatRequest(
270282
organization_id=self.organization.id,
271283
query=prompt,
272284
run_id=None,
273285
insert_index=None,
274286
on_page_context=on_page_context,
275287
page_name=page_name,
276-
user_org_context=collect_user_org_context(
277-
self.user, self.organization, request=request
278-
),
288+
user_org_context=user_org_context,
279289
intelligence_level=self.intelligence_level,
280290
is_interactive=self.is_interactive,
281291
enable_coding=self.enable_coding,
292+
enable_code_mode_tools=self.enable_code_mode_tools,
293+
user_auth_token=user_auth_token,
282294
)
283295

284296
if self.max_iterations is not None:
@@ -344,6 +356,7 @@ def continue_run(
344356
page_name: str | None = None,
345357
artifact_key: str | None = None,
346358
artifact_schema: type[BaseModel] | None = None,
359+
request: Request | None = None,
347360
) -> int:
348361
"""
349362
Continue an existing Seer Explorer session. This allows you to add follow-up queries to an ongoing conversation.
@@ -366,6 +379,14 @@ def continue_run(
366379
if bool(artifact_schema) != bool(artifact_key):
367380
raise ValueError("artifact_key and artifact_schema must be provided together")
368381

382+
user_auth_token = (
383+
create_explorer_api_token(self.user, self.organization)
384+
if self.enable_code_mode_tools
385+
and self.user
386+
and not isinstance(self.user, AnonymousUser)
387+
else None
388+
)
389+
369390
chat_body: ExplorerChatRequest = ExplorerChatRequest(
370391
organization_id=self.organization.id,
371392
query=prompt,
@@ -375,6 +396,8 @@ def continue_run(
375396
page_name=page_name,
376397
is_interactive=self.is_interactive,
377398
enable_coding=self.enable_coding,
399+
enable_code_mode_tools=self.enable_code_mode_tools,
400+
user_auth_token=user_auth_token,
378401
)
379402

380403
if prompt_metadata:

src/sentry/seer/explorer/client_utils.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99

1010
import logging
1111
import time
12-
from datetime import datetime
12+
from datetime import datetime, timedelta
1313
from typing import Any, NotRequired, TypedDict
1414

1515
import orjson
1616
from django.conf import settings
1717
from django.contrib.auth.models import AnonymousUser
18+
from django.utils import timezone
1819
from rest_framework.request import Request
1920
from urllib3 import BaseHTTPResponse, HTTPConnectionPool
2021

@@ -57,6 +58,7 @@ class ExplorerChatRequest(TypedDict):
5758
intelligence_level: NotRequired[str]
5859
is_interactive: NotRequired[bool]
5960
enable_coding: NotRequired[bool]
61+
enable_code_mode_tools: NotRequired[bool]
6062
project_id: NotRequired[int]
6163
query_metadata: NotRequired[dict[str, str]]
6264
artifact_key: NotRequired[str]
@@ -68,6 +70,7 @@ class ExplorerChatRequest(TypedDict):
6870
metadata: NotRequired[dict[str, Any]]
6971
is_context_engine_enabled: NotRequired[bool]
7072
max_iterations: NotRequired[int]
73+
user_auth_token: NotRequired[str | None]
7174

7275

7376
class ExplorerRunsRequest(TypedDict):
@@ -297,6 +300,34 @@ def collect_user_org_context(
297300
}
298301

299302

303+
def create_explorer_api_token(user: SentryUser | RpcUser, organization: Organization) -> str | None:
304+
"""Create a short-lived read-only API token for Seer to call back into Sentry."""
305+
try:
306+
from sentry.models.apitoken import ApiToken
307+
from sentry.types.token import AuthTokenType
308+
from sentry.users.models.user import User as UserModel
309+
310+
real_user = UserModel.objects.get(id=user.id)
311+
token = ApiToken.objects.create(
312+
user=real_user,
313+
token_type=AuthTokenType.USER,
314+
scoping_organization_id=organization.id,
315+
scope_list=[
316+
"org:read",
317+
"project:read",
318+
"event:read",
319+
"alerts:read",
320+
"member:read",
321+
"team:read",
322+
],
323+
expires_at=timezone.now() + timedelta(hours=1),
324+
)
325+
return token.plaintext_token
326+
except Exception:
327+
logger.exception("Failed to create short-lived API token for Seer Explorer")
328+
return None
329+
330+
300331
def fetch_run_status(run_id: int, organization: Organization) -> SeerRunState:
301332
"""Fetch current run status from Seer."""
302333
body = ExplorerStateRequest(run_id=run_id, organization_id=organization.id)

tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,18 @@ def test_post_new_conversation_calls_client(self, mock_client_class: MagicMock):
6969
assert response.status_code == 200
7070
assert response.data == {"run_id": 456}
7171
mock_client_class.assert_called_once_with(
72-
self.organization, ANY, is_interactive=True, enable_coding=False
72+
self.organization,
73+
ANY,
74+
is_interactive=True,
75+
enable_coding=False,
76+
enable_code_mode_tools=False,
7377
)
7478
mock_client.start_run.assert_called_once_with(
7579
prompt="What is this error about?",
7680
on_page_context=None,
7781
page_name=None,
7882
override_ce_enable=True,
83+
request=ANY,
7984
)
8085

8186
@patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient")
@@ -104,6 +109,7 @@ def test_post_new_conversation_enable_coding(self, mock_client_class: MagicMock)
104109
ANY,
105110
is_interactive=True,
106111
enable_coding=feature_enabled and option_enabled,
112+
enable_code_mode_tools=False,
107113
)
108114

109115
@patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient")
@@ -121,14 +127,19 @@ def test_post_continue_conversation_calls_client(self, mock_client_class: MagicM
121127
assert response.status_code == 200
122128
assert response.data == {"run_id": 789}
123129
mock_client_class.assert_called_once_with(
124-
self.organization, ANY, is_interactive=True, enable_coding=False
130+
self.organization,
131+
ANY,
132+
is_interactive=True,
133+
enable_coding=False,
134+
enable_code_mode_tools=False,
125135
)
126136
mock_client.continue_run.assert_called_once_with(
127137
run_id=789,
128138
prompt="Follow up question",
129139
insert_index=2,
130140
on_page_context=None,
131141
page_name=None,
142+
request=ANY,
132143
)
133144

134145
@patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient")
@@ -152,6 +163,7 @@ def test_post_continue_conversation_enable_coding(self, mock_client_class: Magic
152163
ANY,
153164
is_interactive=True,
154165
enable_coding=feature_enabled and option_enabled,
166+
enable_code_mode_tools=False,
155167
)
156168

157169
@patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient")

0 commit comments

Comments
 (0)