Skip to content

Commit da5b959

Browse files
authored
feat(seer): Grant seer:write to Seer agent tokens by default (#127503)
## Summary Grants the `seer:write` scope to Seer agents so that invocations of the agent don't require `event:write` permission from the user to run any of the autofix steps. Previously the user would have to accept the permission from the seer chat agent anytime they wanted to iterate on the auto fix generated plan from the Seer chat panel. This allows the agent to now have the correct scope so that it isn't required. ## Test plan [x] Autofix iterations on RCA or Plan don't require a permission acceptance.
1 parent 9bd9d0f commit da5b959

3 files changed

Lines changed: 68 additions & 5 deletions

File tree

‎src/sentry/seer/agent_token.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ def readonly_scopes() -> frozenset[str]:
9393
return frozenset(settings.SENTRY_READONLY_SCOPES)
9494

9595

96+
AGENT_DEFAULT_WRITE_SCOPES = frozenset({"seer:write"})
97+
98+
9699
def resolve_minting_principal(
97100
user: MintingUser, auth: AuthenticatedToken | None
98101
) -> MintingPrincipal:
@@ -156,10 +159,14 @@ def compute_token_scopes(
156159
session_id: str,
157160
requested_scopes: Iterable[str] | None = None,
158161
) -> list[str]:
159-
"""De-escalation rule: ``caller_scopes ∩ (read-only ∪ approved grants)``, optionally
160-
narrowed by ``requested_scopes``. Never exceeds the caller's own authority."""
162+
"""De-escalation rule: ``caller_scopes ∩ (read-only ∪ default writes ∪ approved grants)``,
163+
optionally narrowed by ``requested_scopes``. Never exceeds the caller's own authority."""
161164
caller = set(caller_scopes)
162-
allowed = readonly_scopes() | active_grant_scopes(organization_id, user_id, session_id)
165+
allowed = (
166+
readonly_scopes()
167+
| AGENT_DEFAULT_WRITE_SCOPES
168+
| active_grant_scopes(organization_id, user_id, session_id)
169+
)
163170
effective = caller & allowed
164171
if requested_scopes is not None:
165172
effective &= set(requested_scopes)

‎tests/sentry/seer/endpoints/test_group_ai_autofix.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050

5151
USER_CONTEXT_MAX_LENGTH = 10000
5252

53+
AGENT_TOKEN_SECRET = "test-seer-api-shared-secret-thirty-two-bytes!"
54+
AGENT_TOKEN_FLAG = "organizations:seer-agent-token-flow"
55+
5356

5457
def _user_context_length_calls(mock_distribution: Mock) -> list:
5558
return [
@@ -474,6 +477,55 @@ def test_post_triggers_autofix_agent(self, mock_trigger_explorer):
474477
assert response.data["run_id"] == 123
475478
mock_trigger_explorer.assert_called_once()
476479

480+
def _mint_agent_token(self) -> str:
481+
self.login_as(user=self.user)
482+
response = self.client.post(
483+
f"/api/0/organizations/{self.organization.slug}/agent/token/",
484+
data={"sessionId": "agent-session"},
485+
format="json",
486+
)
487+
assert response.status_code == 200, response.content
488+
return response.data["token"]
489+
490+
@override_settings(SEER_API_SHARED_SECRET=AGENT_TOKEN_SECRET)
491+
@with_feature(AGENT_TOKEN_FLAG)
492+
@patch("sentry.seer.endpoints.group_ai_autofix.trigger_autofix_agent")
493+
def test_post_agent_token_runs_analysis_without_approval(self, mock_trigger_explorer):
494+
group = self.create_group()
495+
run = self.create_seer_run(organization=self.organization, seer_run_state_id=123)
496+
mock_trigger_explorer.return_value = run
497+
token = self._mint_agent_token()
498+
499+
response = self.client.post(
500+
self._get_url(group.id),
501+
data={"step": "root_cause"},
502+
format="json",
503+
HTTP_AUTHORIZATION=f"Bearer {token}",
504+
)
505+
506+
assert response.status_code == 202, response.data
507+
mock_trigger_explorer.assert_called_once()
508+
509+
@override_settings(SEER_API_SHARED_SECRET=AGENT_TOKEN_SECRET)
510+
@with_feature(AGENT_TOKEN_FLAG)
511+
@patch("sentry.seer.endpoints.group_ai_autofix.trigger_push_changes")
512+
def test_post_agent_token_open_pr_requires_approval(self, mock_push_changes):
513+
group = self.create_group()
514+
token = self._mint_agent_token()
515+
516+
response = self.client.post(
517+
self._get_url(group.id),
518+
data={"step": "open_pr", "run_id": 123},
519+
format="json",
520+
HTTP_AUTHORIZATION=f"Bearer {token}",
521+
)
522+
523+
assert response.status_code == 403, response.data
524+
assert (
525+
response["WWW-Authenticate"] == 'Bearer error="insufficient_scope", scope="event:write"'
526+
)
527+
mock_push_changes.assert_not_called()
528+
477529
@patch("sentry.seer.endpoints.group_ai_autofix.trigger_push_changes")
478530
@patch("sentry.seer.endpoints.group_ai_autofix.trigger_autofix_agent")
479531
def test_post_seer_write_token_runs_analysis_but_cannot_open_pr(

‎tests/sentry/seer/endpoints/test_organization_agent_token.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ def _grant(self, *, organization=None, session_id="s1", scopes=("org:write",)):
661661
scope_list=list(scopes),
662662
)
663663

664-
def test_mint_defaults_to_readonly(self) -> None:
664+
def test_mint_defaults_to_readonly_and_default_writes(self) -> None:
665665
self.login_as(self.owner)
666666
with self.feature(FLAG):
667667
resp = self._mint(sessionId="s1")
@@ -672,7 +672,11 @@ def test_mint_defaults_to_readonly(self) -> None:
672672
assert claims["org"] == self.org.id
673673
assert claims["sid"] == "s1"
674674
assert "org:write" not in claims["scopes"]
675-
assert set(claims["scopes"]) <= agent_token.readonly_scopes()
675+
assert "event:write" not in claims["scopes"]
676+
assert "seer:write" in claims["scopes"]
677+
assert set(claims["scopes"]) <= (
678+
agent_token.readonly_scopes() | agent_token.AGENT_DEFAULT_WRITE_SCOPES
679+
)
676680

677681
def test_feature_off_is_not_found(self) -> None:
678682
self.login_as(self.owner)

0 commit comments

Comments
 (0)