How to Implement GuardRail Validation for Agent Input and Output in Embabel
Embabel provides a plug‑in validation mechanism called Guard Rails that lets you enforce safety, quality, and policy rules on both user input reaching an LLM and assistant output returned by the model.
The embabel/embabel-agent repository includes a comprehensive validation system within the embabel‑agent‑api module. This system intercepts messages at the API boundary, allowing you to implement custom validation logic that runs automatically before requests are sent to language models and after responses are received.
Core GuardRail Interfaces
The validation framework centers on three interfaces located in embabel-agent-api/src/main/kotlin/com/embabel/agent/api/validation/guardrails/.
GuardRail serves as the base contract. It defines the validate(String, Blackboard): ValidationResult method that all implementations must provide. This interface lives in [GuardRail.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/validation/guardrails/GuardRail.kt).
UserInputGuardRail extends the base interface specifically for incoming user messages. The source file [UserInputGuardRail.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/validation/guardrails/UserInputGuardRail.kt) marks the entry point for input validation.
AssistantMessageGuardRail handles outgoing assistant messages and adds an overload that receives a ThinkingResponse. This allows validation of both the final text and intermediate reasoning steps. See [AssistantMessageGuardRail.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/validation/guardrails/AssistantMessageGuardRail.kt).
Global Registration via GlobalGuardRailsRegistry
All guard‑rails are discovered and instantiated by the GlobalGuardRailsRegistry class, a Spring @Component located in [GlobalGuardRailsRegistry.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/guardrails/GlobalGuardRailsRegistry.kt).
The registry reads two comma‑separated configuration properties:
embabel.agent.guardrails.user-inputembabel.agent.guardrails.assistant-message
During its @PostConstruct init() method, the registry calls instantiateGuardRails<T>() for each configured class list. This method uses reflection via ClassUtils.forName to load classes and validates that they implement the required interface. If a class cannot be instantiated and embabel.agent.guardrails.fail-on-error=true is set, the registry throws a GuardRailInstantiationException. Otherwise, errors are logged and the offending guard‑rail is skipped.
The registry exposes the created singletons through static helpers: getUserInputGuardRails() and getAssistantMessageGuardRails().
Where GuardRails Are Enforced
User‑input validation occurs in the GuardedOperationsProxyFactory (located in the embabel-agent-common module). This factory wraps LLM calls and iterates over the global user‑input guard‑rails before sending the request to the model. If any guard returns a failure ValidationResult, the pipeline immediately throws a GuardRailViolationException, aborting the request.
Assistant‑output validation runs after the model produces a response. Production code in embabel-agent-openai and other model modules follows the pattern demonstrated in [StreamingChatClientOperationsGuardRailTest.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/test/kotlin/com/embabel/agent/spi/support/springai/streaming/StreamingChatClientOperationsGuardRailTest.kt). After receiving an AssistantMessage or ThinkingResponse, each registered AssistantMessageGuardRail is invoked sequentially.
Creating Custom GuardRail Implementations
Follow these steps to add custom validation logic:
- Implement the appropriate interface — Choose
UserInputGuardRailfor input validation orAssistantMessageGuardRailfor output validation. - Override the
validatemethod — ReturnValidationResult.success()on pass orValidationResult.failure("reason")on violation. - Register the class — Add the fully‑qualified class name to the appropriate property in
application.properties. - Configure error handling — Optionally set
embabel.agent.guardrails.fail-on-error=trueto force startup failure if instantiation fails.
Example: Profanity Filter for User Input
package com.mycompany.guardrails
import com.embabel.agent.api.validation.guardrails.UserInputGuardRail
import com.embabel.common.core.validation.ValidationResult
import com.embabel.common.util.loggerFor
import com.embabel.common.core.Blackboard
class ProfanityFilterGuardRail : UserInputGuardRail {
private val logger = loggerFor<ProfanityFilterGuardRail>()
private val banned = setOf("badword1", "badword2")
override val name: String = "ProfanityFilter"
override fun validate(input: String, blackboard: Blackboard): ValidationResult {
val containsBanned = banned.any { input.contains(it, ignoreCase = true) }
return if (containsBanned) {
logger.warn("Profanity detected in user input")
ValidationResult.failure("Profanity is not allowed")
} else {
ValidationResult.success()
}
}
}
Register in application.properties:
embabel.agent.guardrails.user-input=com.mycompany.guardrails.ProfanityFilterGuardRail
Example: Output Length Guard for Assistant Messages
package com.mycompany.guardrails
import com.embabel.agent.api.validation.guardrails.AssistantMessageGuardRail
import com.embabel.common.core.validation.ValidationResult
import com.embabel.common.util.loggerFor
import com.embabel.common.core.Blackboard
class MaxLengthGuardRail : AssistantMessageGuardRail {
private val logger = loggerFor<MaxLengthGuardRail>()
private val MAX_LENGTH = 500
override val name: String = "MaxLengthGuard"
override fun validate(input: String, blackboard: Blackboard): ValidationResult =
if (input.length > MAX_LENGTH) {
logger.warn("Assistant message exceeds max length")
ValidationResult.failure("Response too long")
} else {
ValidationResult.success()
}
}
Register in application.properties:
embabel.agent.guardrails.assistant-message=com.mycompany.guardrails.MaxLengthGuardRail
Handling GuardRail Violations
When a guard‑rail signals failure, the core pipeline throws a GuardRailViolationException defined in [GuardRailViolationException.kt](https://github.com/embabel/embabel-agent/blob/main/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/validation/guardrails/GuardRailViolationException.kt). You should catch this exception in your higher‑level service layer to return user‑friendly error messages or trigger alternative handling workflows.
Summary
- Guard Rails in Embabel provide a plug‑in mechanism for validating LLM inputs and outputs via the
embabel-agent-apimodule. - Three core interfaces define the contracts:
GuardRail,UserInputGuardRail, andAssistantMessageGuardRail. - GlobalGuardRailsRegistry automatically discovers and instantiates guard‑rails from the
embabel.agent.guardrails.user-inputandembabel.agent.guardrails.assistant-messageproperties. - Enforcement points include
GuardedOperationsProxyFactoryfor input validation and model-specific handlers for output validation. - Implementations must return
ValidationResultobjects and can triggerGuardRailViolationExceptionon failure.
Frequently Asked Questions
What is the difference between UserInputGuardRail and AssistantMessageGuardRail?
UserInputGuardRail validates raw text strings sent by users before they reach the LLM, while AssistantMessageGuardRail validates text generated by the model before it returns to the user. The assistant variant includes an additional overload validate(ThinkingResponse<*>, Blackboard) that allows inspection of intermediate reasoning steps, not just the final text output.
How do I register multiple GuardRails for the same stage?
Add multiple fully‑qualified class names as a comma‑separated list in the appropriate property. For example: embabel.agent.guardrails.user-input=com.example.FirstGuardRail,com.example.SecondGuardRail. The GlobalGuardRailsRegistry will instantiate and apply each guard‑rail in the order specified.
What happens if a GuardRail class cannot be instantiated at startup?
If embabel.agent.guardrails.fail-on-error is set to true, the application will fail to start with a GuardRailInstantiationException. If left as false (the default), the registry logs the error and continues without that specific guard‑rail, allowing the application to start with reduced validation coverage.
Can I access the conversation history or metadata inside a GuardRail?
Yes. Every validate method receives a Blackboard parameter that provides access to contextual data, conversation history, and other metadata stored in the current execution context. You can use this blackboard to implement stateful validation rules that depend on previous messages or session-specific information.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →