Guardrails
A guardrail decides whether an agent may perform an action on a resource — and blocks it before the action runs.
guardrails For: governance & controlThe guardrail system is a framework, not a fixed set of classes: GuardrailEnv
is a runtime-checkable protocol with a single core method, evaluate(request).
Any object that implements it — yours or a registered one — plugs into the
wrappers, pipelines, and batch evaluators the same way.
How it works
- Write a guardrail — subclass
BaseGuardrailEnvand implement a pureevaluate(request)that returnsALLOWorDENY. - Compose — chain guardrails in a
GuardrailPipelineand stackWrappers (cache, timeout, audit) around any of them. - Fail closed — wrap the outermost layer in
DenyByDefaultWrapperso any exception becomesDENY, then gate the action onresult.is_allowed.
flowchart LR
A["Agent action"] --> B["DenyByDefaultWrapper"]
B --> C["GuardrailPipeline"]
C -- "ALLOW" --> D["Action runs"]
C -- "DENY" --> X["Action blocked"]
B -- "exception" --> X
Install
pip install briefcase-ai[guardrails]from briefcase.guardrails import ( BaseGuardrailEnv, EvalRequest, EvalResult, Effect,)Write a Guardrail
Subclass BaseGuardrailEnv and implement evaluate(). It receives an
EvalRequest and returns an EvalResult carrying an Effect (ALLOW or
DENY).
from briefcase.guardrails import BaseGuardrailEnv, EvalRequest, EvalResult, Effect
class TierGuardrail(BaseGuardrailEnv): """Allow access to a resource only for agents on the right tier."""
_name = "tier-check"
def __init__(self, allowed_tiers): self._allowed = set(allowed_tiers)
def evaluate(self, request: EvalRequest) -> EvalResult: tier = request.context.get("tier") if tier in self._allowed: return EvalResult( effect=Effect.ALLOW, guardrail_name=self._name, reason=f"tier '{tier}' is permitted", ) return EvalResult( effect=Effect.DENY, guardrail_name=self._name, reason=f"tier '{tier}' is not permitted", )
env = TierGuardrail(allowed_tiers={"standard", "premium"})
request = EvalRequest( agent="support-bot", action="invoke", resource="kb/internal-articles", context={"tier": "premium"},)
result = env.evaluate(request)print(result.is_allowed) # Trueprint(result.effect) # Effect.ALLOWprint(result.reason) # "tier 'premium' is permitted"EvalRequest
EvalRequest( agent="support-bot", # who is acting action="invoke", # what they want to do resource="kb/internal-articles", # what they want to act on context={"tier": "premium"}, # attributes the guardrail evaluates request_id=None, # optional correlation id)EvalResult
| Field | Description |
|---|---|
effect | Effect.ALLOW or Effect.DENY |
guardrail_name | Name of the guardrail that produced the result |
reason | Human-readable explanation |
is_allowed | True when effect == Effect.ALLOW |
policy_id | Optional identifier of the policy applied |
lakefs_sha | Optional commit the policy was loaded from |
eval_time_ms | Evaluation time |
Register and Instantiate
Register a guardrail by string id and instantiate it with make(), the same
register/make split used by Gymnasium. This lets callers construct guardrails
without importing the implementation.
from briefcase.guardrails import register, make
# entry_point is a "module:ClassName" string — in your code that is the import# path of your guardrail, e.g. "myapp.guardrails:TierGuardrail".register( id="tier-check-v1", entry_point=f"{__name__}:TierGuardrail", kwargs={"allowed_tiers": ["standard", "premium"]},)
env = make("tier-check-v1") # uses the registered kwargsenv = make("tier-check-v1", allowed_tiers=["premium"]) # override per callChain Guardrails with a Pipeline
- Define each check as its own guardrail (resource allowlist, tier check, rate check) so each stays small and testable.
- Order them in a
GuardrailPipeline, cheapest and most restrictive first. - Pick a mode —
FIRST_DENYshort-circuits on the firstDENY(the default and the cheapest);ALLandMAJORITYrun every stage.
GuardrailPipeline evaluates a request through several guardrails in order. By
default it short-circuits on the first DENY.
from briefcase.guardrails import ( BaseGuardrailEnv, EvalRequest, EvalResult, Effect, GuardrailPipeline, PipelineMode,)
class ResourceAllowlist(BaseGuardrailEnv): _name = "resource-allowlist"
def __init__(self, allowed): self._allowed = set(allowed)
def evaluate(self, request: EvalRequest) -> EvalResult: ok = request.resource in self._allowed return EvalResult( effect=Effect.ALLOW if ok else Effect.DENY, guardrail_name=self._name, reason="resource permitted" if ok else "resource not allowlisted", )
pipeline = GuardrailPipeline( stages=[ ResourceAllowlist(allowed={"kb/internal-articles"}), TierGuardrail(allowed_tiers={"standard", "premium"}), ], mode=PipelineMode.FIRST_DENY,)
request = EvalRequest( agent="support-bot", action="invoke", resource="kb/internal-articles", context={"tier": "premium"},)
outcome = pipeline.evaluate(request)print(outcome.is_allowed) # Trueprint(len(outcome.individual_results)) # one result per stage that ranPipelineMode options:
| Mode | Behavior |
|---|---|
FIRST_DENY | Stop on the first DENY (default) |
ALL | Evaluate every stage; DENY if any stage denies |
MAJORITY | Majority vote across stages |
flowchart LR
A["EvalRequest"] --> B["ResourceAllowlist"]
B -- "DENY" --> X["Block (short-circuit)"]
B -- "ALLOW" --> C["TierGuardrail"]
C -- "DENY" --> X
C -- "ALLOW" --> D["Allow"]
Composable Wrappers
A wrapper is a GuardrailEnv, so wrappers stack around any guardrail.
from briefcase.guardrails import ( CacheWrapper, TimeoutWrapper, DenyByDefaultWrapper, Effect,)
env = DenyByDefaultWrapper( TimeoutWrapper( CacheWrapper(TierGuardrail(allowed_tiers={"premium"})), max_ms=10.0, fallback_effect=Effect.DENY, ))
result = env.evaluate(request)| Wrapper | Effect |
|---|---|
CacheWrapper | Caches results with a TTL |
TimeoutWrapper | Falls back (default DENY) if evaluation exceeds max_ms |
AuditWrapper | Records every (request, result) for observability |
SamplingWrapper | Evaluates a fraction of requests; allows the rest |
DenyByDefaultWrapper | Catches exceptions and returns DENY |
ViolationModeWrapper | Converts DENY to ALLOW for soft-deny workflows |
Gate the Action (fail-closed)
A guardrail only governs an action if you actually gate on its result. Evaluate
before the side effect runs, treat anything that is not an explicit ALLOW as
a deny, and let the DenyByDefaultWrapper turn any unexpected exception into a
block.
from briefcase.guardrails import DenyByDefaultWrapper, EvalRequest
# Outermost layer fails closed: any exception inside becomes DENY.gate = DenyByDefaultWrapper(TierGuardrail(allowed_tiers={"premium"}))
def classify_ticket(ticket, *, agent="support-bot"): request = EvalRequest( agent=agent, action="invoke", resource="kb/internal-articles", context={"tier": ticket["tier"]}, )
try: result = gate.evaluate(request) except Exception: # Belt and suspenders: even if the gate itself raises, deny. raise PermissionError("guardrail evaluation failed; action blocked")
if not result.is_allowed: raise PermissionError(f"action denied: {result.reason}")
# Authorized — now it is safe to run the side effect. # return run_classification(ticket) # call your model / tools hereWhere this fits
Guardrails are the Control act of the journey: enforce the rule before the action runs. Once an action is authorized, route it; once it has run, capture it.