···107107 REMOVED = 'removed'
108108109109110110+class EmptyEntityError(Exception):
111111+ """Raised when an entity has no labels and error_on_empty is True."""
112112+113113+ def __init__(self, entity: EntityT[Any], label: str):
114114+ self.entity = entity
115115+ self.label = label
116116+ super().__init__(
117117+ f"Entity '{entity.type}/{entity.id}' has no labels. "
118118+ f'This may indicate the labels service failed to fetch data. '
119119+ f"(Checked for label: '{label}')"
120120+ )
121121+122122+110123class HasLabelArguments(ArgumentsBase):
111124 entity: EntityT[Any]
112125 """An Entity to check for a label on."""
···118131 """Optional: A specific status to check for. Default is 'added'."""
119132 min_label_age: Optional[TimeDeltaT] = None
120133 """Optional: Checks to see if the label was added after a period of time"""
134134+ error_on_empty: bool = False
135135+ """Optional: If True, raise EmptyEntityError when the entity has no labels at all.
136136+137137+ WARNING: Only use this for safety-critical rules where a false negative (due to labels
138138+ service returning empty data on failure) could cause dangerous rule evaluations, such as
139139+ incorrectly allowing a known-bad entity through. Do not use this for general label checks.
140140+141141+ This parameter should only be used when the entity type is guaranteed to have at least one
142142+ label in the labels service. If the entity type is not guaranteed to have labels, the rule
143143+ should add a dummy/sentinel label to the entity before calling HasLabel with error_on_empty=True.
144144+ """
121145122146123147@dataclass
···128152 status: str
129153 min_label_age: Optional[TimeDeltaT]
130154 desired_status: Optional[_SimpleStatus]
155155+ error_on_empty: bool
131156132157133158class HasLabel(
···165190166191 validation_context.add_error(message='unknown label', span=arguments.label.argument_span, hint=hint)
167192193193+ def _check_error_on_empty(
194194+ self, entity: EntityT[Any], label: str, entity_labels: EntityLabels, error_on_empty: bool
195195+ ) -> None:
196196+ """Fail-closed check for labels service data integrity.
197197+198198+ When error_on_empty is True, raises EmptyEntityError if the entity has zero labels.
199199+ This catches cases where the labels service may have failed to fetch data and returned
200200+ an empty default response. Without this check, `not HasLabel(...)` would incorrectly
201201+ evaluate to True, potentially allowing dangerous entities through safety rules.
202202+ """
203203+ if error_on_empty and len(entity_labels.labels) == 0:
204204+ raise EmptyEntityError(entity, label)
205205+168206 def _execute(
169207 self, execution_context: ExecutionContext, arguments: BatchableHasLabelArguments, entity_labels: EntityLabels
170208 ) -> bool:
209209+ self._check_error_on_empty(arguments.entity, arguments.label, entity_labels, arguments.error_on_empty)
171210 desired_manual = _ManualType.get(arguments.manual)
172211 desired_delay = TimeDeltaT.inner_from_optional(arguments.min_label_age)
173212 label_state = entity_labels.labels.get(arguments.label)
···237276 status=arguments.status.value,
238277 min_label_age=arguments.min_label_age,
239278 desired_status=self.desired_status,
279279+ error_on_empty=arguments.error_on_empty,
240280 )
241281242282 def get_batch_routing_key(self, arguments: BatchableHasLabelArguments) -> str: