Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cbdde3a
feat(seer): Gate agent chat messages on Seer credits
srest2021 Nov 6, 2026
c247328
feat(seer): Check Seer credits before Autofix steps and Open PR
srest2021 Nov 6, 2026
e2855ba
ref(seer): Keep the token-billed check inside can_run_seer_operation
srest2021 Nov 6, 2026
33f5287
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
d70d6cb
ref(seer): Split has_enough_seer_credits out of can_run_seer_operation
srest2021 Nov 6, 2026
58a1b63
ref(seer): Combine the credit check flag conditions
srest2021 Nov 6, 2026
8856cd4
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
c49f7ed
Merge remote-tracking branch 'origin/master' into srest2021/CW-3061-a…
srest2021 Nov 6, 2026
8e5ca8f
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
0ab3b10
feat(seer): Check Seer credits before replying to an Autofix single run
srest2021 Nov 6, 2026
69f5488
Merge remote-tracking branch 'origin/master' into srest2021/CW-3061-a…
srest2021 Nov 6, 2026
0f648e8
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
a3dae0d
ref(seer): Check the credit check flag first
srest2021 Nov 6, 2026
f738ddf
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
94b979c
test(seer): Align credit check tests with their files
srest2021 Nov 6, 2026
302de4f
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
0ec42da
Merge remote-tracking branch 'origin/master' into srest2021/CW-3061-a…
srest2021 Nov 6, 2026
6f7cb6f
ref(seer): Leave single run replies out of credit gating
srest2021 Nov 6, 2026
99eb2ac
fix(seer): Don't let API callers claim the agentic triage referrer
srest2021 Nov 6, 2026
e8f415e
ref(seer): Clarify why the endpoint ignores the agentic triage referrer
srest2021 Nov 6, 2026
396779d
feat(seer): Check Seer credits before replying to an Autofix single run
srest2021 Nov 6, 2026
90c5db5
cleaning up diff
srest2021 Nov 6, 2026
ff4faae
ref(seer): Restore the billing comment in trigger_autofix_agent
srest2021 Nov 6, 2026
b86335d
cleaning up
srest2021 Nov 6, 2026
245aca9
fix ordering
srest2021 Nov 6, 2026
80bba23
Restore comment
srest2021 Nov 6, 2026
914a60c
ref(seer): Keep all billing skip rules in check_autofix_billing
srest2021 Nov 6, 2026
28e5990
refactor billing helper
srest2021 Nov 6, 2026
766bc10
move logs out of helper
srest2021 Nov 6, 2026
6c0043d
small test fix:
srest2021 Nov 6, 2026
1c7c190
cleaning up tests
srest2021 Nov 6, 2026
4e289d3
Merge remote-tracking branch 'origin/master' into srest2021/CW-3061-a…
srest2021 Nov 7, 2026
451f3bd
ref(seer-billing): Pass organization to check_autofix_billing
sentry-junior[bot] Nov 9, 2026
ab0d0aa
ref(seer-billing): Check Autofix billing only in dispatch
srest2021 Nov 9, 2026
89bdfdc
ref(seer-billing): Drop the agentic triage referrer exemption
srest2021 Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 29 additions & 21 deletions src/sentry/seer/autofix/autofix_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from scm.types import GetBranchProtocol, GetRepositoryProtocol
from sentry_sdk import traces

from sentry import features, quotas
from sentry import features
from sentry.analytics.events.autofix_events import (
AiAutofixAgentHandoffEvent,
AiAutofixCodeChangesCompletedEvent,
Expand All @@ -23,18 +23,19 @@
AiAutofixSolutionCompletedEvent,
AiAutofixSolutionStartedEvent,
)
from sentry.constants import ENABLE_SEER_CODING_DEFAULT, DataCategory
from sentry.constants import ENABLE_SEER_CODING_DEFAULT
from sentry.integrations.services.integration import integration_service
from sentry.models.group import Group
from sentry.seer.agent.client import SeerAgentClient
from sentry.seer.agent.client_models import SeerRunState
from sentry.seer.autofix.analytics import record_autofix_event
from sentry.seer.autofix.commit_author import SeerCommitAuthor
from sentry.seer.autofix.constants import AutofixReferrer
from sentry.seer.autofix.exceptions import NoSeerQuotaException
from sentry.seer.autofix.exceptions import NoSeerQuotaException, SeerOutOfCreditsException
from sentry.seer.autofix.feature.dispatch import (
AutofixFeatureArgs,
autofix_run_targets_other_issue,
check_autofix_billing,

Check warning on line 38 in src/sentry/seer/autofix/autofix_agent.py

View check run for this annotation

@sentry/warden / warden: wrdn-authz

Client-supplied referrer bypasses Seer credit checks

`check_autofix_billing` skips credits when `referrer == AGENTIC_TRIAGE` (`night_shift`), but the group autofix API accepts any `AutofixReferrer` from the request body—spoof `referrer: "night_shift"` to run Autofix without credits. Gate the free path on a server-only signal (like `allow_free_cohort`), not client referrer.
require_autofix_run_for_group,
trigger_autofix_feature,
)
Expand Down Expand Up @@ -63,6 +64,7 @@
is_free_cohort_org,
read_preference_from_sentry_db,
)
from sentry.seer.credits import SeerOperation
from sentry.seer.entrypoints.operator import (
SeerActivityAttribution,
SeerAutofixOperator,
Expand Down Expand Up @@ -461,19 +463,13 @@
"""
outcome = "success"
try:
# check billing quota for triggering a new autofix run
# Free cohort orgs bypass quota only when called from agentic triage
# (allow_free_cohort=True). The API endpoint never sets this flag,
# so manual triggers still require quota.
if run_id is None:
skip_quota_check = allow_free_cohort and is_free_cohort_org(group.organization)
if not skip_quota_check:
has_budget: bool = quotas.backend.check_seer_quota(
org_id=group.organization.id,
data_category=DataCategory.SEER_AUTOFIX,
)
if not has_budget:
raise NoSeerQuotaException()
check_autofix_billing(
Comment thread
srest2021 marked this conversation as resolved.
Resolved
group,
step.seer_operation,
referrer,
is_new_run=run_id is None,
skip_quota_for_free_cohort=allow_free_cohort,
)

Check warning on line 472 in src/sentry/seer/autofix/autofix_agent.py

View check run for this annotation

@sentry/warden / warden: security-review

Autofix credit check bypassed by spoofing agentic triage referrer

Passing referrer `night_shift` on the Autofix API skips Seer credit checks for token-billed orgs, so callers can run Autofix steps and Open PR without credits. Gate the free path on a server-only signal (like allow_free_cohort), not client-supplied referrer.

Check warning on line 472 in src/sentry/seer/autofix/autofix_agent.py

View check run for this annotation

@sentry/warden / warden: wrdn-authz

[LRE-22B] Client-supplied referrer bypasses Seer credit checks (additional location)

`check_autofix_billing` skips credits when `referrer == AGENTIC_TRIAGE` (`night_shift`), but the group autofix API accepts any `AutofixReferrer` from the request body—spoof `referrer: "night_shift"` to run Autofix without credits. Gate the free path on a server-only signal (like `allow_free_cohort`), not client referrer.
Comment thread
srest2021 marked this conversation as resolved.
Resolved
Comment thread
srest2021 marked this conversation as resolved.
Resolved

if run_id is not None:
_assert_existing_run_belongs_to_group(group, run_id)
Expand Down Expand Up @@ -548,7 +544,12 @@
actor_user_id=actor_user_id,
)
return feature_run
except (NoSeerQuotaException, SeerPermissionError, PrIterationNoPullRequestException):
except (
NoSeerQuotaException,
SeerOutOfCreditsException,
SeerPermissionError,
PrIterationNoPullRequestException,
):
outcome = "declined"
raise
except BaseException:
Expand Down Expand Up @@ -839,6 +840,17 @@
):
raise PermissionDenied("Code generation is disabled for this organization")

seer_run = SeerRun.objects.filter(
organization_id=group.organization.id, seer_run_state_id=run_id
).first()
# Open PR always continues a run, so it never uses quota.
check_autofix_billing(
group,
SeerOperation.AUTOFIX_OPEN_PR,
seer_run.referrer if seer_run else None,
is_new_run=False,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-assisted review, posted on behalf of @billyvg.

Same question for Open PR. When a user clicks Open PR (issue page or Slack) on a run that agentic triage started, this skips the credit check, because the run's referrer is night_shift. A user continuing a step on that same run is checked under their own referrer, though. The completion hook's automatic push for agentic triage runs does need the run's referrer to stay free. If user-triggered pushes should be checked, one option is to use the run's referrer only when referrer == AutofixReferrer.ON_COMPLETION_HOOK, and the caller's referrer otherwise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in #130552


Check warning on line 853 in src/sentry/seer/autofix/autofix_agent.py

View check run for this annotation

@sentry/warden / warden: security-review

[ZBH-K2L] Autofix credit check bypassed by spoofing agentic triage referrer (additional location)

Passing referrer `night_shift` on the Autofix API skips Seer credit checks for token-billed orgs, so callers can run Autofix steps and Open PR without credits. Gate the free path on a server-only signal (like allow_free_cohort), not client-supplied referrer.
client = get_autofix_agent_client(group, user=user)

if state is None:
Expand All @@ -856,10 +868,6 @@
)
)

seer_run = SeerRun.objects.filter(
organization_id=group.organization.id, seer_run_state_id=run_id
).first()

# Draft when review-request/CI-green is enabled; mark ready once green fires.
client.push_changes(
run_id,
Expand Down
4 changes: 4 additions & 0 deletions src/sentry/seer/autofix/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ class NoSeerQuotaException(Exception):
"""Raised when an Autofix run cannot consume Seer quota."""


class SeerOutOfCreditsException(Exception):
"""Raised when the organization doesn't have enough Seer credits for an Autofix operation."""


class IssueSummaryUnavailable(Exception):
"""Raised when an issue summary cannot be generated for an expected reason."""

Expand Down
93 changes: 80 additions & 13 deletions src/sentry/seer/autofix/feature/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
)
from sentry.seer.agent.on_completion_hook import extract_hook_definition
from sentry.seer.autofix.constants import AutofixReferrer
from sentry.seer.autofix.exceptions import NoSeerQuotaException
from sentry.seer.autofix.exceptions import NoSeerQuotaException, SeerOutOfCreditsException
from sentry.seer.autofix.feature.models import (
FEATURE_ID,
LEGACY_FEATURE_ID,
Expand All @@ -41,6 +41,7 @@
get_automation_handoff,
is_free_cohort_org,
)
from sentry.seer.credits import SeerOperation, has_enough_seer_credits

Check warning on line 44 in src/sentry/seer/autofix/feature/dispatch.py

View check run for this annotation

@sentry/warden / warden: security-review

[ZBH-K2L] Autofix credit check bypassed by spoofing agentic triage referrer (additional location)

Passing referrer `night_shift` on the Autofix API skips Seer credit checks for token-billed orgs, so callers can run Autofix steps and Open PR without credits. Gate the free path on a server-only signal (like allow_free_cohort), not client-supplied referrer.
from sentry.seer.models.run import SeerAgentRun, SeerRun
from sentry.seer.models.seer_api_models import UNKNOWN_RUN_ID_FOR_GROUP, SeerPermissionError
from sentry.users.models.user import User
Expand Down Expand Up @@ -201,18 +202,45 @@
)


def trigger_autofix_feature(
group: Group,
args: AutofixFeatureArgs,
) -> SeerRun:
# Avoid a circular import through the legacy Autofix dispatcher.
from sentry.seer.autofix.on_completion_hook import AutofixOnCompletionHook
def _check_autofix_credits(group: Group, operation: SeerOperation, referrer: str | None) -> None:
"""
Raise if the organization can't afford the operation under token-based
credits. Agentic triage runs stay free.
"""
if (
not features.has("organizations:seer-credit-checks", group.organization)
or referrer == AutofixReferrer.AGENTIC_TRIAGE
):
return

Check warning on line 214 in src/sentry/seer/autofix/feature/dispatch.py

View check run for this annotation

@sentry/warden / warden: security-review

[ZBH-K2L] Autofix credit check bypassed by spoofing agentic triage referrer (additional location)

Passing referrer `night_shift` on the Autofix API skips Seer credit checks for token-billed orgs, so callers can run Autofix steps and Open PR without credits. Gate the free path on a server-only signal (like allow_free_cohort), not client-supplied referrer.

Check failure on line 214 in src/sentry/seer/autofix/feature/dispatch.py

View check run for this annotation

@sentry/warden / warden: sentry-backend-bugs

Client-supplied night_shift referrer skips Autofix credit checks

`_check_autofix_credits` returns early when `referrer == AGENTIC_TRIAGE`, but the group Autofix API accepts any `AutofixReferrer` from the request body—including `night_shift`—so token-billed orgs can run steps/Open PR without credits. Gate the free path on a server-only signal like `allow_free_cohort`, not client referrer.

Check warning on line 214 in src/sentry/seer/autofix/feature/dispatch.py

View check run for this annotation

@sentry/warden / warden: wrdn-authz

[LRE-22B] Client-supplied referrer bypasses Seer credit checks (additional location)

`check_autofix_billing` skips credits when `referrer == AGENTIC_TRIAGE` (`night_shift`), but the group autofix API accepts any `AutofixReferrer` from the request body—spoof `referrer: "night_shift"` to run Autofix without credits. Gate the free path on a server-only signal (like `allow_free_cohort`), not client referrer.
if not has_enough_seer_credits(group.organization, operation):
logger.info(
"autofix_feature.dispatch.out_of_credits",
extra={
"group_id": group.id,
"organization_id": group.organization.id,
"operation": operation.value,
"referrer": referrer,
},
)
raise SeerOutOfCreditsException()

is_new_run = args.existing_run_id is None

def _check_autofix_quota(
group: Group,
referrer: str | None,
*,
is_new_run: bool,
allow_free_cohort: bool,
) -> bool:
"""
Raise if the organization has no seat-based Seer quota left. Returns whether
the step uses quota, which is recorded once the run starts.
"""
# Free cohort orgs bypass quota only when called from agentic triage
# (allow_free_cohort=True). Not exposed via the API.
skip_quota = is_new_run and args.allow_free_cohort and is_free_cohort_org(group.organization)
if is_new_run and not skip_quota:
skip_quota = is_new_run and allow_free_cohort and is_free_cohort_org(group.organization)
uses_quota = is_new_run and not skip_quota
if uses_quota:
has_budget: bool = quotas.backend.check_seer_quota(
org_id=group.organization.id,
data_category=DataCategory.SEER_AUTOFIX,
Expand All @@ -223,11 +251,50 @@
extra={
"group_id": group.id,
"organization_id": group.organization.id,
"referrer": args.referrer.value,
"referrer": referrer,
},
)
raise NoSeerQuotaException()

return uses_quota


def check_autofix_billing(
group: Group,
operation: SeerOperation,
referrer: str | None,
*,
is_new_run: bool,
skip_quota_for_free_cohort: bool = False,
) -> bool:
"""
Raise if the organization can't pay for the operation. Token-billed orgs are
checked for credits, everyone else for seat-based quota. Returns whether the
operation uses quota.
"""
if features.has("organizations:token-billed-seer-enabled", group.organization):
_check_autofix_credits(group, operation, referrer)
return False
return _check_autofix_quota(
group, referrer, is_new_run=is_new_run, allow_free_cohort=skip_quota_for_free_cohort
)


def trigger_autofix_feature(
group: Group,
args: AutofixFeatureArgs,
) -> SeerRun:
# Avoid a circular import through the legacy Autofix dispatcher.
from sentry.seer.autofix.on_completion_hook import AutofixOnCompletionHook

uses_quota = check_autofix_billing(
group,
args.step.seer_operation,
args.referrer,
is_new_run=args.existing_run_id is None,
skip_quota_for_free_cohort=args.allow_free_cohort,
)

single_run = should_use_single_run(
group,
args.step,
Expand Down Expand Up @@ -316,36 +383,36 @@
extras["stopping_point"] = stopping_point.value

user_org_context = collect_user_org_context(args.user, group.organization)
if is_new_run:
if args.existing_run_id is None:
run = client.start_feature_run(
feature_id=SINGLE_RUN_FEATURE_ID if single_run else FEATURE_ID,
payload=payload.dict(),
referrer=args.referrer.value,
user_org_context=user_org_context,
proxy_headers=get_proxy_headers(),
agent_run_options=agent_run_options,
title=f"Autofix — {short_id}" if single_run else f"Autofix RCA — {short_id}",
flush=args.flush,
extras=extras,
)
elif args.existing_run_id is not None:
existing_agent_run = require_autofix_run_for_group(group, args.existing_run_id)

run = client.continue_feature_run(
existing_agent_run=existing_agent_run,
payload=payload.dict(),
referrer=args.referrer.value,
user_org_context=user_org_context,
proxy_headers=get_proxy_headers(),
agent_run_options=agent_run_options,
)
else:
raise Exception("Unhandled run_id branch, this should never happen")

if is_new_run and not skip_quota:
if uses_quota:
quotas.backend.record_seer_run(
group.organization.id, group.project.id, DataCategory.SEER_AUTOFIX
)

Check warning on line 415 in src/sentry/seer/autofix/feature/dispatch.py

View check run for this annotation

@sentry/warden / warden: security-review

[ZBH-K2L] Autofix credit check bypassed by spoofing agentic triage referrer (additional location)

Passing referrer `night_shift` on the Autofix API skips Seer credit checks for token-billed orgs, so callers can run Autofix steps and Open PR without credits. Gate the free path on a server-only signal (like allow_free_cohort), not client-supplied referrer.

Check warning on line 415 in src/sentry/seer/autofix/feature/dispatch.py

View check run for this annotation

@sentry/warden / warden: wrdn-authz

[LRE-22B] Client-supplied referrer bypasses Seer credit checks (additional location)

`check_autofix_billing` skips credits when `referrer == AGENTIC_TRIAGE` (`night_shift`), but the group autofix API accepts any `AutofixReferrer` from the request body—spoof `referrer: "night_shift"` to run Autofix without credits. Gate the free path on a server-only signal (like `allow_free_cohort`), not client referrer.

metrics.incr(
"autofix_feature.trigger",
Expand Down
14 changes: 14 additions & 0 deletions src/sentry/seer/autofix/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from enum import StrEnum

from sentry.seer.autofix.utils import AutofixStoppingPoint
from sentry.seer.credits import SeerOperation


class AutofixStep(StrEnum):
Expand All @@ -13,6 +14,19 @@ class AutofixStep(StrEnum):
CODE_CHANGES = "code_changes"
PR_ITERATION = "pr_iteration"

@property
def seer_operation(self) -> SeerOperation:
"""The Seer credit check operation corresponding to this step."""
match self:
case AutofixStep.ROOT_CAUSE:
return SeerOperation.AUTOFIX_ROOT_CAUSE
case AutofixStep.SOLUTION:
return SeerOperation.AUTOFIX_SOLUTION
case AutofixStep.CODE_CHANGES:
return SeerOperation.AUTOFIX_CODE_CHANGES
case AutofixStep.PR_ITERATION:
return SeerOperation.AUTOFIX_PR_ITERATION

@staticmethod
def from_autofix_stopping_point(
autofix_stopping_point: AutofixStoppingPoint,
Expand Down
21 changes: 14 additions & 7 deletions src/sentry/seer/credits.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,11 @@ class SeerOperationConfig:
}


def can_run_seer_operation(organization: Organization, operation: SeerOperation) -> bool:
"""Whether the organization is token-billed and has enough Seer credits to run the operation."""
if not features.has("organizations:seer-credit-checks", organization) or not features.has(
"organizations:token-billed-seer-enabled", organization
):
return True

def has_enough_seer_credits(organization: Organization, operation: SeerOperation) -> bool:
"""
Checks whether the organization has enough Seer credits to run the operation
for callers that already checked the credit check and token-billed flags.
"""
config = SEER_OPERATIONS[operation]
response = seer_credit.backend.check_seer_credits(
CheckSeerCreditsRequest(
Expand All @@ -77,3 +75,12 @@ def can_run_seer_operation(organization: Organization, operation: SeerOperation)
)
)
return response.has_enough_credits


def can_run_seer_operation(organization: Organization, operation: SeerOperation) -> bool:
"""Whether the organization is token-billed and has enough Seer credits to run the operation."""
if not features.has("organizations:seer-credit-checks", organization) or not features.has(
"organizations:token-billed-seer-enabled", organization
):
return True
return has_enough_seer_credits(organization, operation)
Loading
Loading