Skip to content

fix: emit LLMGuardrailCompletedEvent for a malformed guardrail result - #8648

Merged
Vidit-Ostwal merged 1 commit into
crewAIInc:mainfrom
subhashpolisetti:fix/guardrail-malformed-result-closes-event
Oct 27, 2026
Merged

Vidit-Ostwal merged 1 commit into
crewAIInc:mainfrom
subhashpolisetti:fix/guardrail-malformed-result-closes-event

Conversation

@subhashpolisetti

@subhashpolisetti subhashpolisetti commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Related issue

Fixes #8647

Summary

Follow-up to #8403. process_guardrail converts the guardrail's return value with GuardrailResult.from_tuple
after the try block that #8403 added, so a guardrail returning the wrong shape (for example (False, 42) or a
three-item tuple) raised without a LLMGuardrailCompletedEvent. The started scope stayed on the event stack:
Agent.kickoff and Crew.kickoff logged event pairing mismatches, and later events in the same context got the
wrong parent.

The conversion now runs inside the try, so these errors take the same terminal path as a guardrail that raises.
The caller still gets the same exception, and a well-formed result is handled exactly as before. All four callers
(Agent, LiteAgent, and the sync and async Task paths) go through process_guardrail, so this one change
covers them.

Verification

  • Tests added or updated for the changed behavior
  • Relevant tests and quality checks pass locally

Added to test_guardrail_events_on_exception.py, next to the #8403 tests:

  • test_a_malformed_guardrail_result_still_reports_the_validation_it_started (non-string-error, three-tuple):
    both fail on main with assert 0 == 1 (no completed event) and pass with the fix.
  • test_a_valid_guardrail_result_reports_one_completed_event: control, a well-formed (False, "too short") still
    emits exactly one completed event. Passes on main and with the fix.

The issue repro through Agent.kickoff() and Crew.kickoff() now emits both events, leaves the scope stack empty,
and logs no pairing mismatch.

  • lib/crewai/tests/: 6349 passed, 44 skipped (Python 3.13)
  • guardrail tests also pass on Python 3.10
  • ruff check, ruff format, mypy clean

Additional context

None


Note

Low Risk
Small control-flow change in event emission only; behavior for valid guardrails and raised errors stays the same.

Overview
Moves GuardrailResult.from_tuple inside the try in process_guardrail so malformed guardrail return values (wrong tuple length or non-string error payload) no longer skip the terminal LLMGuardrailCompletedEvent that #8403 added for raised exceptions.

Those failures now follow the same path as guardrail raises: one completed event with success=False and an error string, then the original exception is re-raised. Valid (bool, …) results are unchanged.

Tests cover malformed returns ((False, 42), three-tuples) and confirm well-formed failures still emit a single completed event.

Reviewed by Cursor Bugbot for commit 59ef43c. Bugbot is set up for automated code reviews on this repo. Configure here.

process_guardrail emits LLMGuardrailStartedEvent, then converts the
guardrail's return value with GuardrailResult.from_tuple. That conversion
ran outside the try block crewAIInc#8403 added, so a guardrail returning the wrong
shape, such as (False, 42) or a three-item tuple, raised without a
LLMGuardrailCompletedEvent. The started scope stayed on the event stack:
Agent.kickoff and Crew.kickoff then logged event pairing mismatches and
later events in the same context got the wrong parent.

Convert the result inside the try block so these errors take the same
terminal path as a guardrail that raises. The caller still gets the same
exception.
@strix-security

strix-security Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Strix Security Review

✅ No open security findings.

PR summary: Moves the conversion of a guardrail's return value into a `GuardrailResult` inside the existing error-handling `try` block of `process_guardrail`, so a guardrail that returns the wrong shape now emits a matching `LLMGuardrailCompletedEvent` instead of leaving the started event unpaired. Adds tests for malformed returns and a well-formed control case.

Review summary

The only functional change moves GuardrailResult.from_tuple(result) inside the try block in process_guardrail, so a guardrail returning a malformed shape now takes the same terminal path as one that raises: a single LLMGuardrailCompletedEvent with success=False is emitted and the bare raise re-raises the identical exception to all four callers. No exception is swallowed or converted, no retry semantics change, and no authentication, authorization, crypto, deserialization, file, network, or query path is touched. Bandit and semgrep on the changed source reported nothing. The remaining changes are tests only. No security issues found.

Review 1 on this PR · 59ef43c · requested Oct 27 06:32 UTC when the PR was opened


Reviewed by Strix
Re-run review · Configure security review settings

@Vidit-Ostwal Vidit-Ostwal left a comment

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.

Reviewed the malformed guardrail-result path and its regression coverage. The conversion now correctly remains inside the exception boundary, ensuring the started guardrail event receives a failed completion event while preserving the original exception.

@Vidit-Ostwal
Vidit-Ostwal merged commit 00ef9ea into crewAIInc:main Oct 27, 2026
61 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]A guardrail that returns a malformed result leaves LLMGuardrailStartedEvent without its completed event

2 participants