Skip to content
Docs for briefcase-ai v3.3.0see what’s new.

Guardrails

A guardrail decides whether an agent may perform an action on a resource — and blocks it before the action runs.

guardrails For: governance & control

The 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

  1. Write a guardrail — subclass BaseGuardrailEnv and implement a pure evaluate(request) that returns ALLOW or DENY.
  2. Compose — chain guardrails in a GuardrailPipeline and stack Wrappers (cache, timeout, audit) around any of them.
  3. Fail closed — wrap the outermost layer in DenyByDefaultWrapper so any exception becomes DENY, then gate the action on result.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

Terminal window
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) # True
print(result.effect) # Effect.ALLOW
print(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

FieldDescription
effectEffect.ALLOW or Effect.DENY
guardrail_nameName of the guardrail that produced the result
reasonHuman-readable explanation
is_allowedTrue when effect == Effect.ALLOW
policy_idOptional identifier of the policy applied
lakefs_shaOptional commit the policy was loaded from
eval_time_msEvaluation 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 kwargs
env = make("tier-check-v1", allowed_tiers=["premium"]) # override per call

Chain Guardrails with a Pipeline

  1. Define each check as its own guardrail (resource allowlist, tier check, rate check) so each stays small and testable.
  2. Order them in a GuardrailPipeline, cheapest and most restrictive first.
  3. Pick a modeFIRST_DENY short-circuits on the first DENY (the default and the cheapest); ALL and MAJORITY run 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) # True
print(len(outcome.individual_results)) # one result per stage that ran

PipelineMode options:

ModeBehavior
FIRST_DENYStop on the first DENY (default)
ALLEvaluate every stage; DENY if any stage denies
MAJORITYMajority 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)
WrapperEffect
CacheWrapperCaches results with a TTL
TimeoutWrapperFalls back (default DENY) if evaluation exceeds max_ms
AuditWrapperRecords every (request, result) for observability
SamplingWrapperEvaluates a fraction of requests; allows the rest
DenyByDefaultWrapperCatches exceptions and returns DENY
ViolationModeWrapperConverts 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 here

Where 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.