ENGINEERING BLOG · AGENT CONTROL PLANE
Establishing Control Boundaries for AI Agents: The Difference Between Prompts and Policy Enforcement
A system prompt is a suggestion the model may or may not follow. The moment an agent has real capability, hope stops being an acceptable strategy and a control plane becomes essential.
Key takeaways
- Prompt instructions aren't an execution boundary. They can shape a model's behavior, but without an agent control plane behind them, they don't guarantee a file change, email, payment, or tool call actually gets stopped.
- The violation often lives in the path, not the step. A read that's authorized, followed by a send that's authorized, can still add up to a leak that neither step shows on its own.
- Preventive policy today is a circuit breaker, not an interceptor. It stops the nextinvocation of an agent once a rule has been violated. It doesn't reach into a running task and redact a value mid-flight.
- Guardrail Detection is passive by design. It classifies what's already in your traces and tells you what's missing. It blocks nothing.
- A violation is a record, not a remediation. Revocation, rollback, and quarantine are human workflows today, triggered from the evidence Traccia gives you.
Two Categories, Not One Guardrail
When an AI agent can touch a customer's record, send a message in someone's name, move money, or alter production data, a confident-sounding system prompt is not a control. It's a suggestion the model may or may not follow, and the moment that agent has real capability, hope stops being an acceptable strategy.
This is the problem Traccia is built around, and it's worth being precise about what that means in practice, because "AI agent governance" gets used to describe a lot of different things. Traccia organizes agent policy enforcement into two categories, borrowed from a much older discipline in security and audit, but applied to the mechanics of AI agents:
- Preventive policy is about stopping an agent from running again once it's shown it shouldn't be trusted to.
- Detective policy is about knowing, reliably, what an agent actually did, whether that's a guardrail firing, a policy rule getting violated, or a gap in coverage nobody noticed.
It's worth being honest about the shape of each of these today, because they're not identical to how "prevention" and "detection" get used in other security contexts. Below is how we think about it, one layer at a time.
What Happens When There's No Real Boundary?
A line that exists only in text is easy to cross without noticing. Here's what crossing an invisible boundary looks like in practice: no exploit, no jailbreak, no malicious prompt. It only requires an agent doing exactly what it was told to do, one authorized step at a time.
A support agent, five steps, zero red flags:
- Authenticates as a support workflow.
- Reads a customer record for an approved purpose.
- Summarizes the record locally.
- Opens an email tool that is normally allowed.
- Sends the summary to an unapproved personal address.
Every individual step looks fine in isolation. The read is authorized. The email tool is permitted. Nothing about step 4 is inherently wrong. The violation lives in the path connecting steps 2 and 5, not in any single action. A system that only checks "is this tool call allowed right now" will miss it completely, because it's evaluating requests one at a time instead of evaluating the trajectory the agent is actually on.
This is exactly why instructions like "never send customer data to an external address," buried in a system prompt, aren't enforcement. They can shape a model's behavior, but they don't guarantee an email actually gets stopped. Real enforcement has to sit outside the model.
Preventive Policy: How it Actually Works
Traccia's preventive mechanism is built around two SDK-level functions: observe, which is observability only, and govern, which adds a runtime policy check on top of observability and requires the Traccia platform.
govern calls the Traccia API to check an agent's status before the wrapped function body runs:
from traccia import init, governfrom traccia.governance import AgentBlockedError
init(api_key="...", endpoint="https://api.traccia.ai/v2/traces")
@govern(agent_id="my-agent", fail_open=False, name="run_agent")def run_agent(prompt: str) -> str: return call_llm(prompt)On the platform side, you define policies as declarative rules: metric, operator, threshold, and, for aggregate rules, a time window and an aggregation function.
| Rule type | Metrics available | Evaluated | Enforcement mode |
|---|---|---|---|
| Per Trace | Cost, execution time, token counts, tool call count, allowed models | After a single run is ingested | Observe, Warn, or Block |
| Aggregate | Same metrics, rolled up over a time window | AVG, SUM, MAX, or MIN over the window | Observe, Warn, or Block |
Here's the part that matters most for understanding what "preventive" really means in the product today: policy evaluation happens after a trace is ingested: the trace is already stored by the time a rule runs. And Warn or Block don't interrupt the run that triggered them. They arm the next @govern() call. If that next call has fail_open=False, Traccia raises AgentBlockedError before the function body executes, stopping that invocation from starting. Ingest itself never rejects a trace because of a policy.
Warning
Two things are useful to know if you're setting this up:
- Simulate Last 7 Days: lets you backtest a draft policy against the last week of traces before activating it, so you can see how many runs would have matched without turning on enforcement blind.
fail_openon@govern(): is what determines fail-closed behavior. Set it toFalseif you want a status-check failure or a Block to actually stop the next run, rather than let it through.
Guardrail Detection: A Separate, Passive Layer
Alongside policy enforcement, Traccia ships a guardrail detection engine that runs as an OTel span processor. It's important to be clear about what this is: passive detection, zero runtime enforcement. It doesn't block anything. It classifies signals already present in your traces and tells you what it found, and what it thinks is missing.
Findings come in three tiers, each with different confidence:
| Tier | How it's found | Confidence |
|---|---|---|
| A: Explicit | You annotate a function with @observe(as_type="guardrail") or wrap a check in guardrail_span() | High, the only way to prove a specific guardrail exists |
| B: Provider-native | Captured automatically from structured fields like llm.finish_reason, llm.stop_reason, or llm.safety_ratings (OpenAI, Azure OpenAI, Google GenAI, Anthropic) | Automatic, no annotation needed |
| C: Heuristic | Pattern-matching on tool error messages for denial keywords (permission, denied, unauthorized, forbidden, not allowed) | Always low, deliberately does NOT count as coverage |
Seeing something that looks like a denial isn't the same as having a guardrail, which is why Tier C never counts toward coverage on its own.
When a trace's root span ends, a missing-guardrail evaluator infers what the agent's capabilities were (does it call an LLM, handle user text, use tools) and reports which guardrail categories you'd expect but didn't detect, each with its own confidence level. You can suppress specific categories for agents where they genuinely don't apply, like internal batch pipelines.
Info
- Out-of-band guardrails are invisible: An API gateway or proxy-level filter outside tracing won't show up unless it writes span attributes.
- Presence doesn't mean correctness: A PII check that always returns
Falselooks identical from the trace to one that works. - Absence doesn't necessarily mean missing: No guardrail span could mean missing guardrail, or out-of-band filter.
Detective Policy: Violations, Not Automated Remediation
On the detective side, when a policy rule is exceeded, Traccia records a violation, not an automatic remediation action.
Policy Violation Lifecycle
State transitions recorded in decision log
A later compliant run doesn't retroactively close an earlier open one. That's deliberate. Every evaluation, matched or not, is written to a Decision Log, including quiet "No Match" rows, so you can confirm a rule is actually running rather than silently broken.
To be clear about what's not here yet: there's no automated credential revocation, data quarantine, or rollback built into the product today. Remediation, once a violation is Open, is a human workflow: review it in the Governance Hub, acknowledge or resolve it, and act outside the system if the situation calls for it.
Where This Sits in the Broader Platform
All of the above is one piece of a larger idea: the same trace that tells you what an agent did should be the thing that makes it governable, not a separate system bolted on afterward. That's what we mean when we call Traccia an agent control plane: a single layer of visibility, control, and evidence sitting under every agent you run. Traccia is organized around four pillars that build on one another:
1. Visibility
Full traces of every agent run: sessions, tool calls, prompts, and errors, captured from Python and TypeScript.
2. Intelligence
Cost attribution under sampling, plus datasets, scorers, and experiments to prove quality before you promote.
3. Control
Spend limits, retry caps, and custom rules enforced at the agent boundary: observe and govern.
4. Certification
An AI system registry, incidents, and audit-ready evidence exports generated from the traces you already collect.
The SDK itself is open source and OpenTelemetry-native, so it works standalone against your own backend (Jaeger, Grafana Tempo, Zipkin, or any OTLP-compatible collector), with the hosted platform layered on top when you want enforcement, evaluations, and governance evidence.
The Bottom Line
Guardrail Detection tells you what protections actually fired, or should have existed but didn't, inside a single run. Policy enforcement tells you when an agent's behavior, aggregated or per-run, has crossed a line you defined, and can stop that agent from running again until someone looks at it.
Neither one, by itself, gives you a system that inspects and blocks an individual tool call mid-flight based on its specific arguments. If that's the kind of control your use case genuinely needs, it's worth knowing that today, going into a design.
That's what Traccia gives you today: Guardrail Detection surfaces what's protected and what's missing inside a run, and Preventive policy, through govern and @govern(), gives you a circuit breaker to stop the next invocation once a violation has been recorded. It's not a black box you only open after something's gone wrong. The same trace data drives detection, policy evaluation, and the evidence trail behind both. The goal isn't confidence in the model. It's confidence in the system that's actually watching it.
References
- Traccia Governance SDK Docs (https://traccia.ai/docs/sdk/governance)
- Traccia Guardrail Detection Engine (https://traccia.ai/docs/sdk/guardrails)
- Traccia Platform Policy Reference (https://traccia.ai/docs/platform/policies)
- Blog: AI Observability vs AI Governance (https://traccia.ai/blog/ai-observability-vs-ai-governance)
- Blog: Guardrails & Policy Enforcement for OpenAI Agents (https://traccia.ai/blog/openai-agents-guardrails-policy-enforcement)
Make the Trace Governable
Your agents are already making decisions in production. Do you know what your guardrails actually caught, and what they missed?