Lab 02 — Bedrock Guardrails: a Content-Safety Policy Engine

Phase 24 · Lab 02 · Phase README · Warmup

The problem

Bedrock Guardrails answers a different question than Bedrock AgentCore's Cedar Policy (Phase 20, Lab 02) does, and mixing them up is a fast way to sound junior in an interview:

  • Cedar/IAM (access control) decides whether a principal may take an action on a resource — "may this agent call the refund tool?" It is an authorization decision.
  • Guardrails (content safety) decides whether a piece of text is safe to pass through — "does this sentence contain hate speech, PII, a denied topic, or an ungrounded claim?" It is a content decision, applied to the prompt going IN and the generation coming OUT.

An agent needs both. Guardrails is what you build here: five policies — denied topics, content filters (six categories, each with a configurable severity threshold), word filters, PII detection (block-or-mask), and contextual grounding — applied at two points in the request lifecycle: the input guardrail (before the model runs at all) and the output guardrail (after generation, before the caller sees it). Every policy that actually intervenes produces a trace entry, mirroring the real Guardrails assessments shape.

What you build

PieceWhat it doesThe lesson
DeniedTopic / _score_content / word-filter scantopic, content, and word policiesmultiple independent filters, each with its own detection logic
PII_PATTERNS + block-or-maskregex PII detection (email, phone, SSN, credit card)masking preserves utility; blocking destroys it — the tradeoff is per-entity-type, configurable
groundedness_score / relevance_scoredeterministic token-overlap grounding checka generation can be fluent AND wrong; grounding catches that content filters can't
GuardrailsEngine.apply_input / apply_outputthe two guardrail application pointsdenied input never reaches the model; ungrounded output never reaches the caller
invoke_with_guardrailsthe full pipeline: input guardrail → model → output guardrailproves the model is skipped entirely on an input block
TraceEntry / GuardrailActionwhich specific rule fired, and whythe real Guardrails intervention trace, in miniature

File map

FileRole
lab.pyyour implementation (fill the # TODOs)
solution.pyreference implementation + worked example (python solution.py)
test_lab.py26 tests: each policy in isolation, thresholds, mask-vs-block, grounding, full pipeline, trace shape
requirements.txtpytest only

Run it

pytest test_lab.py -v                       # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v   # the reference (green)
python solution.py                          # worked example

Success criteria

  • A benign input/output passes through untouched: action=NONE, blocked=False, no trace.
  • A denied-topic input is blocked before the model runsmodel_called=False, proven by a call counter on the injected model_fn staying at zero.
  • A PII-bearing output is masked, not blocked, when the entity type's action is MASK; it IS blocked when the action is BLOCK — configurable per entity type.
  • An ungrounded generated answer is blocked by the contextual grounding check, and the trace shows contextualGroundingPolicy.
  • Content-filter severity thresholds are configurable per category and visibly change the block/no-block outcome for the identical input text.
  • Every intervention produces a TraceEntry with policy, name, action, and detail.
  • All 26 tests pass under both lab and solution.

How this maps to the real stack

  • Denied topics, content filters, word filters, and PII (sensitive information) filters are the real Bedrock Guardrails policy names. Content filters really do cover hate, insults, sexual, violence, misconduct, and prompt-attack categories, each independently scored at NONE/LOW/MEDIUM/HIGH severity with a configurable block threshold per category — exactly the content_thresholds dict here. Real severity scoring is model-based (a classifier), not keyword-matched; the lexicon here is the honest, deterministic stand-in that keeps the threshold mechanics visible and testable.
  • PII filters in real Guardrails detect a long list of built-in entity types (this lab uses the real entity-type names EMAIL, PHONE, US_SOCIAL_SECURITY_NUMBER, CREDIT_DEBIT_CARD_NUMBER among others) and, per type, either block the response or mask/anonymize just the matched span — the same block-vs-mask choice pii_actions models.
  • Contextual grounding checks are a real, distinct Guardrails policy (groundedness + relevance scoring against supplied reference source documents) specifically aimed at catching hallucination that content filters cannot — a fluent, non-toxic, completely made-up answer sails through every other filter and only grounding catches it. Real scoring uses an NLI-style model; our token-overlap ratio is the deterministic, inspectable equivalent of the same idea (does the claim have support in the source?).
  • Where each policy applies: real Guardrails lets you configure most policies (topics, content, words, PII) on the input, the output, or both; contextual grounding is output-only by construction (there's nothing to ground a prompt against). apply_input/apply_output model that split — both call the same _scan, but only apply_output runs grounding. invoke_with_guardrails's ordering (input guardrail → model → output guardrail) is the real request lifecycle, and the "model never runs on a blocked input" behavior is the actual reason Guardrails is cheaper AND safer than a post-hoc content filter: you never pay for or risk a generation you were going to throw away.
  • GuardrailAction (NONE / GUARDRAIL_INTERVENED) and the assessment/trace shape (policy name, what fired, why) mirror the real ApplyGuardrail/Converse response's amazon-bedrock-guardrailAction field and assessments array closely enough to reason about incident response: "which policy fired, on which text, and what did it do about it."
  • The Cedar/Guardrails distinction is the header lesson of this lab: Guardrails is a content-safety layer over TEXT; Cedar (Phase 20, Lab 02) is an access-control layer over ACTIONS. A production agent runs both — Guardrails on every prompt/generation, Cedar on every tool call — and they fail independently: a Guardrails intervention never authorizes a tool call, and a Cedar permit never excuses toxic or hallucinated text.

Limits of the miniature. Real content-filter and denied-topic classification is model-based (a trained classifier scores arbitrary text, not a fixed lexicon); ours is deterministic keyword matching specifically so the threshold semantics are testable without a model in the loop. Real PII detection also covers dozens more entity types (names, addresses, bank routing numbers, passport numbers, and region-specific IDs) with locale-aware patterns; ours covers four illustrative regex-shaped ones. Real contextual grounding uses a purpose-built scoring model; ours is bag-of-words token overlap, the same honest stand-in this track uses everywhere retrieval relevance shows up (Phase 05).

Extensions (your own machine)

  • Swap the keyword lexicon for a real toxicity/classifier model and confirm the _scan pipeline and threshold mechanics are unchanged — proving severity scoring is a pluggable signal.
  • Add contextual grounding using the Phase 05 hashing-embedding cosine instead of raw token overlap, and compare which ungrounded answers each method catches.
  • Add a guardrail version/config registry (like real Bedrock Guardrail versions) so you can A/B two threshold configurations against the same transcript and diff the interventions.
  • Wire invoke_with_guardrails in front of Lab 01's BedrockRuntime.converse so a single call goes through both the capacity model AND the guardrail pipeline, closest to a real request path.

Interview / resume signal

"Built a miniature Bedrock Guardrails engine: denied topics, six-category content filters with per-category configurable severity thresholds, word filters, regex PII detection with a block-or-mask policy per entity type, and a token-overlap contextual grounding check — wired as both an input guardrail (proving the model is never invoked on blocked input) and an output guardrail (proving PII is masked while hallucinated, ungrounded answers are blocked), each producing an intervention trace naming the exact rule that fired."