TealTiger
The ag2.extensions.tealtiger module adds deterministic governance guardrails to AG2 agents. TealTigerMiddleware enforces tool allowlists, detects PII and secrets in tool arguments, tracks per-session cost, provides per-agent kill switches, and produces structured TEEC audit receipts β with no LLM in the governance path.
Installation#
Note
The extension ships with AG2. No additional Python package and no API key are required β all governance evaluation runs inline on the standard library.
Import directly:
Quick Start#
TealTigerMiddleware is a middleware factory β pass the instance straight into an agent's middleware list.
Every tool call now flows through deterministic governance evaluation before execution.
Multi-Stage Defense#
TealTiger governs at two stages of the AG2 agent lifecycle:
flowchart LR
A[π€ User Message] --> B{π‘οΈ Stage 1<br/>on_turn}
B -->|Kill Switch<br/>Check| C[π€ Agent<br/>Processing]
C --> D[π§ Tool Call]
D --> E{π‘οΈ Stage 2<br/>on_tool_execution}
E -->|"β
ALLOW"| F[β‘ Execute Tool]
E -->|"β DENY"| G[π« ToolErrorEvent]
F --> H[π TEEC Receipt]
G --> H
style B fill:#f0fdfa,stroke:#14b8a6,stroke-width:2px
style E fill:#f0fdfa,stroke:#14b8a6,stroke-width:2px
style H fill:#f0fdfa,stroke:#14b8a6,stroke-width:2px
style F fill:#f0fdf4,stroke:#16a34a
style G fill:#fef2f2,stroke:#dc2626 | Hook | Stage | What It Does |
|---|---|---|
on_turn | Turn defense | Kill switch enforcement β frozen agents cannot take turns |
on_tool_execution | Pre-tool defense | Tool allowlist, PII scan, secret scan, cost limit check |
Both stages activate from a single middleware instance. The middleware factory pattern keeps state (decisions, receipts, cost, frozen agents) alive across turns.
Receipts come from the tool stage
A TEECReceipt is emitted for every tool evaluation, whether the call was executed or blocked. Turn-level kill switch denials record a GovernanceDecision but do not emit a receipt.
Governance Modes#
| Mode | Behavior | Use Case |
|---|---|---|
| ENFORCE | Evaluates all policies. Blocks violations with ToolErrorEvent. | Production |
| MONITOR | Evaluates all policies, records decisions, but never blocks. | Staging / shadow testing |
| OBSERVE | Skips policy evaluation. Passes through, still tracks cost and records an OBSERVE_PASSTHROUGH decision. | Initial rollout / lowest overhead |
mode accepts either a GovernanceMode member or its string name, and defaults to GovernanceMode.ENFORCE. Start with MONITOR in staging to see what would be blocked, then switch to ENFORCE in production:
Policy Types#
Tool Allowlist#
Restrict which tools the agent can call. Patterns are matched with fnmatch, so the full glob syntax (*, ?, [seq]) applies:
Any tool not matching the patterns is denied with reason code TOOL_NOT_ALLOWED and risk score 80.
PII Detection#
Block tool calls containing sensitive data in their arguments:
Passing no argument applies all four categories:
| Category | Pattern | Example Match |
|---|---|---|
ssn | \b\d{3}-\d{2}-\d{4}\b | 123-45-6789 |
credit_card | \b(?:\d{4}[-\s]?){3}\d{4}\b | 4111-1111-1111-1111 |
email | Standard email regex | user@example.com |
phone | US phone with optional +1 | +1-555-123-4567 |
Denied with risk score 90. One reason code is appended per matched category, in the form PII_DETECTED:ssn, PII_DETECTED:email, and so on.
Secret Detection#
Block tool calls containing API keys, tokens, or credentials:
| Pattern | What It Catches |
|---|---|
sk-β¦ (20+ chars) | OpenAI-style API keys |
ghp_β¦ (36+ chars) | GitHub personal access tokens |
AKIA⦠(16 chars) | AWS access key IDs |
xox[bpors]-β¦ | Slack tokens |
api_key=β¦ / apikey:β¦ (20+ chars) | Generic API keys |
| PEM private key headers | RSA, EC, DSA private keys |
Denied with reason code SECRET_DETECTED and risk score 95 β the highest of the policy checks.
Cost Limit#
Enforce per-session cost ceilings:
Cumulative cost is tracked across all tool calls using cost_per_call (configurable, defaults to 0.002). Once the limit is reached, further calls are denied with BUDGET_EXCEEDED and risk score 70.
Factory-Level Budget#
In addition to the policy-level cost_limit, you can set a factory-level budget that is always enforced, with no policy needed:
budget_limit defaults to infinity. It is checked before any policy runs, providing a spending cap that no policy ordering can bypass.
Kill Switch#
Freeze an agent immediately β this blocks its turns and all of its tool calls, regardless of policies:
Freezing matches on the exact agent name. There is no wildcard β to freeze several agents, freeze each by name:
The kill switch is enforced at both the turn level (on_turn) and the tool level (on_tool_execution):
- ENFORCE mode: returns
ToolErrorEvent, blocking the entire turn or tool call - MONITOR mode: records a DENY decision but allows the action through
- OBSERVE mode: not evaluated (passthrough)
Kill switch decisions carry reason code AGENT_FROZEN and risk score 100 (maximum).
Audit Trail#
Every governance decision produces a structured GovernanceDecision:
Decision Contract Fields#
| Field | Type | Description |
|---|---|---|
decision_id | str | UUID4 string, unique per decision, for correlation |
action | str | Governance verdict: ALLOW or DENY |
mode | str | Mode the decision was made under: OBSERVE, MONITOR, or ENFORCE |
agent_name | str | Agent that made the call, or unknown |
tool_name | str | Tool that was evaluated (* for turn-level denials) |
reason_codes | list[str] | Machine-readable codes: TOOL_NOT_ALLOWED, PII_DETECTED:{category}, SECRET_DETECTED, BUDGET_EXCEEDED, AGENT_FROZEN, POLICY_ALLOW, OBSERVE_PASSTHROUGH |
risk_score | int | 0 (safe) to 100 (critical) |
evaluation_time_ms | float | Governance latency for this decision |
cost_tracked | float | Cost attributed to this call in USD |
cumulative_cost | float | Running session cost in USD |
timestamp_ms | float | Unix timestamp in milliseconds |
TEEC Receipts#
Every tool evaluation β executed or blocked β produces a TEECReceipt:
| Field | Description |
|---|---|
receipt_id | Unique receipt UUID string |
decision_id | Links back to the governance decision |
agent_name | Agent that triggered the evaluation |
tool_name | Tool that was called |
action | ALLOW or DENY |
execution_outcome | executed, blocked, or error |
reason_codes | Copied from the originating decision |
risk_score | Copied from the originating decision |
policy_digest | Short SHA-256 digest of the active policy set |
timestamp_ms | Unix timestamp in milliseconds |
Factory State API#
The factory exposes the accumulated state directly:
| Member | Description |
|---|---|
decisions | All GovernanceDecision records across all turns |
receipts | All TEECReceipt records across all turns |
total_cost | Cumulative tracked cost in USD |
deny_count | Number of decisions with action == "DENY" |
is_frozen(name) | Whether that agent is currently frozen |
reset() | Clear decisions, receipts, cost, and frozen agents |
Callbacks#
React to governance decisions in real time:
Use Across Multiple Agents#
One governance instance can be attached to several agents β decisions, receipts, cost, and frozen agents are shared across all of them. Here the coordinator delegates to two subagents via Agent.as_tool(), and all three run under the same policy set:
flowchart TB
GOV["π‘οΈ TealTigerMiddleware β Shared Instance<br/>(decisions, receipts, cost, frozen_agents)"]
GOV -->|evaluates| R["π¬ Researcher<br/>tools: search, read_*"]
GOV -->|evaluates| AN["π Analyst<br/>tools: calculator"]
GOV -->|"π§ FROZEN"| W["βοΈ Coordinator"]
style GOV fill:#f0fdfa,stroke:#14b8a6,stroke-width:2px
style R fill:#eff6ff,stroke:#93c5fd
style AN fill:#eff6ff,stroke:#93c5fd
style W fill:#fef2f2,stroke:#fca5a5 Complete Example: Governed Research Agent#
Evaluation Order#
Two checks always run first, in this order. Everything after them is your policy list, walked in the order you declared it β the first DENY wins and short-circuits the rest:
flowchart TD
START([Tool Call Received]) --> S1
S1{{"1οΈβ£ Kill Switch<br/>(risk: 100)"}} -->|frozen| DENY[β DENY]
S1 -->|not frozen| S2
S2{{"2οΈβ£ Factory Budget<br/>(risk: 70)"}} -->|exceeded| DENY
S2 -->|within budget| S3
S3{{"3οΈβ£ Your policies, in list order<br/>first DENY wins"}} -->|violation| DENY
S3 -->|all clean| ALLOW[β
ALLOW]
style S1 fill:#fef2f2,stroke:#fca5a5
style S2 fill:#fef9c3,stroke:#fde047
style S3 fill:#eff6ff,stroke:#93c5fd
style DENY fill:#fef2f2,stroke:#dc2626,stroke-width:2px
style ALLOW fill:#f0fdf4,stroke:#16a34a,stroke-width:2px Each policy type carries its own risk score when it denies:
| Policy | Reason Code | Risk Score |
|---|---|---|
| Kill switch | AGENT_FROZEN | 100 |
| Secret detection | SECRET_DETECTED | 95 |
| PII detection | PII_DETECTED:{category} | 90 |
| Tool allowlist | TOOL_NOT_ALLOWED | 80 |
| Cost limit / factory budget | BUDGET_EXCEEDED | 70 |
Policy order is yours to choose
Because policies are evaluated in the order you pass them, the reason code on a denial depends on that order. If a call violates two policies, only the first one in your list is reported. Put the checks you most want attributed earliest in the list.
If all checks pass, the decision is ALLOW with reason code POLICY_ALLOW and risk score 0.
Architecture#
flowchart TB
subgraph Factory["TealTigerMiddleware (Long-lived factory)"]
STATE["_decisions | _receipts<br/>_frozen_agents | _cumulative_cost"]
API["freeze() | unfreeze() | is_frozen()<br/>.decisions | .receipts | .total_cost"]
end
Factory -->|"creates per turn"| PerTurn
subgraph PerTurn["_TealTigerPerTurn (Per-turn instance)"]
TURN["on_turn()<br/>Kill switch + mode gating"]
TOOL["on_tool_execution()<br/>Policy evaluation + receipts"]
end
PerTurn -->|"writes back"| STATE
style Factory fill:#f0fdfa,stroke:#14b8a6,stroke-width:2px
style PerTurn fill:#eff6ff,stroke:#3b82f6,stroke-width:2px ag2/extensions/tealtiger/
βββ __init__.py # Public API: TealTigerMiddleware, GovernanceMode, GovernancePolicy, GovernanceDecision, TEECReceipt
βββ types.py # GovernanceMode, GovernancePolicy, GovernanceDecision, TEECReceipt
βββ middleware.py # TealTigerMiddleware (factory) + _TealTigerPerTurn (per-turn) + _PII_PATTERNS / _SECRET_PATTERNS
The middleware follows AG2's middleware factory pattern:
TealTigerMiddlewareis the long-lived factory holding shared state (decisions, receipts, frozen agents, cumulative cost)- Calling the factory creates a
_TealTigerPerTurninstance for the turn, holding a reference back to the factory on_turnhandles kill switch enforcement at the turn levelon_tool_executionhandles all policy evaluation at the tool level- Kill switch, budget, and audit state persist across turns and agents
Performance#
Governance runs entirely in-process: no network calls, no LLM inference, and no external service dependencies. Every check is a fnmatch glob or a precompiled re pattern over the serialized tool arguments, so evaluation cost scales with argument size rather than with model latency.
Each decision records its own measured cost in evaluation_time_ms, so you can profile governance overhead against your own workload:
Links#
- TealTiger AG2 integration guide β advanced capabilities beyond the bundled extension
- TealTiger on GitHub (Apache 2.0)
- TealTiger governance platform on PyPI
Maintainer: @nagasatish007 / TealTiger.