Skip to content

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.

pip install ag2

Import directly:

1
2
3
4
5
6
7
from ag2.extensions.tealtiger import (
    GovernanceDecision,
    GovernanceMode,
    GovernancePolicy,
    TEECReceipt,
    TealTigerMiddleware,
)

Quick Start#

TealTigerMiddleware is a middleware factory β€” pass the instance straight into an agent's middleware list.

import asyncio

from ag2 import Agent
from ag2.config import AnthropicConfig
from ag2.extensions.tealtiger import GovernanceMode, GovernancePolicy, TealTigerMiddleware

governance = TealTigerMiddleware(
    mode=GovernanceMode.ENFORCE,
    policies=[
        GovernancePolicy.tool_allowlist(["search", "read_*"]),
        GovernancePolicy.pii_block(["ssn", "credit_card", "email", "phone"]),
        GovernancePolicy.secret_detection(),
        GovernancePolicy.cost_limit(max_per_session=5.0),
    ],
)

async def main() -> None:
    agent = Agent(
        "assistant",
        config=AnthropicConfig(model="claude-sonnet-4-6"),
        middleware=[governance],
    )
    reply = await agent.ask("Summarize the AG2 middleware guide.")
    print(await reply.content())
    print(governance.decisions)
    print(f"Cost: ${governance.total_cost:.4f}")

if __name__ == "__main__":
    asyncio.run(main())

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:

# Shadow mode β€” see what governance would deny without blocking anything
governance = TealTigerMiddleware(
    mode=GovernanceMode.MONITOR,
    policies=[
        GovernancePolicy.tool_allowlist(["search", "read_*"]),
        GovernancePolicy.pii_block(["ssn", "credit_card"]),
    ],
)

# After validation, promote to ENFORCE
governance.mode = GovernanceMode.ENFORCE

Policy Types#

Tool Allowlist#

Restrict which tools the agent can call. Patterns are matched with fnmatch, so the full glob syntax (*, ?, [seq]) applies:

GovernancePolicy.tool_allowlist(["search", "read_*", "github_*"])

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:

GovernancePolicy.pii_block(["ssn", "credit_card", "email", "phone"])

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:

GovernancePolicy.secret_detection()
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:

GovernancePolicy.cost_limit(max_per_session=5.0)

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:

1
2
3
4
5
6
7
governance = TealTigerMiddleware(
    budget_limit=10.0,      # Hard ceiling β€” always enforced
    cost_per_call=0.002,    # Estimated cost per tool call
    policies=[
        GovernancePolicy.tool_allowlist(["search", "read_*"]),
    ],
)

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:

1
2
3
governance.freeze("assistant")      # Block everything for this agent
governance.is_frozen("assistant")   # True
governance.unfreeze("assistant")    # Restore normal governance

Freezing matches on the exact agent name. There is no wildcard β€” to freeze several agents, freeze each by name:

for name in ("researcher", "analyst", "writer"):
    governance.freeze(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:

1
2
3
4
5
6
7
8
9
for decision in governance.decisions:
    print(
        f"[{decision.action}] tool={decision.tool_name} "
        f"agent={decision.agent_name} "
        f"reason_codes={decision.reason_codes} "
        f"risk={decision.risk_score} "
        f"latency={decision.evaluation_time_ms:.2f}ms "
        f"cost=${decision.cumulative_cost:.4f}"
    )

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:

1
2
3
4
5
for receipt in governance.receipts:
    print(
        f"{receipt.execution_outcome} | {receipt.tool_name} | "
        f"agent={receipt.agent_name} | decision={receipt.decision_id}"
    )
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:

def on_deny(decision):
    if decision.action == "DENY":
        alert_ops_team(decision.tool_name, decision.reason_codes)

def on_receipt(receipt):
    audit_log.append(receipt)

governance = TealTigerMiddleware(
    policies=[GovernancePolicy.tool_allowlist(["search"])],
    on_decision=on_deny,       # Called for every decision (ALLOW and DENY)
    on_receipt=on_receipt,     # Called for every TEEC receipt
)

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
import asyncio

from ag2 import Agent
from ag2.config import AnthropicConfig
from ag2.extensions.tealtiger import GovernanceMode, GovernancePolicy, TealTigerMiddleware

# Single governance instance shared across all agents
governance = TealTigerMiddleware(
    mode=GovernanceMode.ENFORCE,
    policies=[
        GovernancePolicy.tool_allowlist(["search", "read_*", "calculator"]),
        GovernancePolicy.pii_block(["ssn", "credit_card", "email"]),
        GovernancePolicy.secret_detection(),
        GovernancePolicy.cost_limit(max_per_session=10.0),
    ],
)
config = AnthropicConfig(model="claude-sonnet-4-6")

async def main() -> None:
    researcher = Agent("researcher", config=config, middleware=[governance])
    analyst = Agent("analyst", config=config, middleware=[governance])
    coordinator = Agent(
        "coordinator",
        config=config,
        middleware=[governance],
        tools=[
            researcher.as_tool(description="Research a topic and return findings."),
            analyst.as_tool(description="Analyze findings and return a summary."),
        ],
    )
    reply = await coordinator.ask("Research AI governance trends, then analyze them.")
    print(await reply.content())
    # Cost, decisions, and receipts are shared across all three agents
    print(f"Total decisions: {len(governance.decisions)}")
    print(f"Denied: {governance.deny_count}")
    print(f"Total cost: ${governance.total_cost:.4f}")
    # Freeze one agent β€” it can no longer take turns or call tools
    governance.freeze("researcher")

if __name__ == "__main__":
    asyncio.run(main())

Complete Example: Governed Research Agent#

import asyncio

from ag2 import Agent
from ag2.config import AnthropicConfig
from ag2.extensions.tealtiger import GovernanceMode, GovernancePolicy, TealTigerMiddleware

governance = TealTigerMiddleware(
    mode=GovernanceMode.ENFORCE,
    budget_limit=10.0,
    cost_per_call=0.003,
    policies=[
        # Tool governance β€” only safe tools
        GovernancePolicy.tool_allowlist(["web_search", "calculator", "read_file"]),
        # Data protection β€” block PII leakage
        GovernancePolicy.pii_block(["ssn", "credit_card", "email", "phone"]),
        # Credential protection β€” block secret leakage
        GovernancePolicy.secret_detection(),
        # Cost governance β€” hard per-session limit
        GovernancePolicy.cost_limit(max_per_session=5.0),
    ],
    on_decision=lambda d: print(f"  [{d.action}] {d.tool_name} β€” {d.reason_codes}"),
)

async def main() -> None:
    agent = Agent(
        "research_assistant",
        config=AnthropicConfig(model="claude-sonnet-4-6"),
        middleware=[governance],
    )
    reply = await agent.ask("Research AI governance frameworks for enterprise deployment.")
    print(await reply.content())
    # Post-run analysis
    allowed = [d for d in governance.decisions if d.action == "ALLOW"]
    denied = [d for d in governance.decisions if d.action == "DENY"]
    print("\n--- Governance Summary ---")
    print(f"Total evaluations: {len(governance.decisions)}")
    print(f"Allowed: {len(allowed)}")
    print(f"Denied: {len(denied)}")
    print(f"Session cost: ${governance.total_cost:.4f}")
    print(f"TEEC receipts: {len(governance.receipts)}")
    for d in denied:
        print(f"  β€’ {d.tool_name}: {d.reason_codes} (risk={d.risk_score})")

if __name__ == "__main__":
    asyncio.run(main())

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:

  • TealTigerMiddleware is the long-lived factory holding shared state (decisions, receipts, frozen agents, cumulative cost)
  • Calling the factory creates a _TealTigerPerTurn instance for the turn, holding a reference back to the factory
  • on_turn handles kill switch enforcement at the turn level
  • on_tool_execution handles 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:

slowest = max(governance.decisions, key=lambda d: d.evaluation_time_ms)
print(f"{slowest.tool_name}: {slowest.evaluation_time_ms:.3f}ms")

Maintainer: @nagasatish007 / TealTiger.