Skip to content

Governance from code

This page covers governing tool calls directly from Python code, without an LLM in the loop: tappass.govern() for wrapping any framework’s tools against POST /v1/govern, the tappass_session context managers for ambient correlation, and the exceptions you handle in enforce mode.

govern(), wrap tools for audit and enforcement

Section titled “govern(), wrap tools for audit and enforcement”
import tappass
tools = tappass.govern(
[search, send_email, read_file],
url="http://localhost:9620", # or TAPPASS_URL
api_key="tp_dev_...", # or TAPPASS_API_KEY
agent_id="spiffe://acme/bot-1",
mode="enforce",
)

govern() works with CrewAI, LangChain, LlamaIndex, Pydantic AI, and plain callables, it introspects each tool object and wraps its hookable function (func, _fn, function, or _run). In mode="enforce", every invocation posts a TOOL_CALL Behavior to /v1/govern before running, and the tool never executes if the kernel blocks it.

Parameter Type Default Description
tools list required Tool objects (any framework) or callables
url str TAPPASS_URL or http://localhost:9620 TapPass server URL
api_key str TAPPASS_API_KEY or "" Bearer key
agent_id str "" Agent identifier. Required in enforce mode for per-agent policy scoping; ignored in audit mode
user_id str "" Optional user ID for audit attribution
mode "audit" | "enforce" "audit" audit = fire-and-forget execution reports; enforce = synchronous pre-call decision
session_id str auto (sdk-<12 hex> in enforce mode) Logical session for path-dependent policy; shared across all wrapped tools
org_id str "" Tenant ID, surfaced in policy input for per-tenant rules
on_pending "raise" | "return" "raise" Behaviour on outcome="needs_approval" (see below)

The default. Every tool execution is reported to the TapPass audit trail asynchronously (tool name, truncated arguments, duration, outcome, error). No decision is requested; the tool always runs.

Before the tool runs, the SDK synchronously posts the Behavior to /v1/govern and acts on the Decision:

Server outcome SDK behaviour
allow Tool executes
block Raises GovernanceBlocked; tool does not execute
needs_approval Raises ApprovalPending (with on_pending="raise") or returns the pending Decision dict (on_pending="return"). Resume by re-submitting the identical call, which returns allow once a human approves
escalate Legacy path: transparently long-polls the approval (wait=True) and converts approve → allow, deny/timeout/cancel → GovernanceBlocked
unreachable / 5xx Raises GovernanceUnavailable, fail-closed. Set TAPPASS_FAIL_OPEN=1 to allow instead (the synthetic allow is flagged with details.fail_open=True in audit)
from tappass import ApprovalPending, GovernanceBlocked, GovernanceUnavailable
try:
governed_tools[0](query="...")
except GovernanceBlocked as e:
print(e.reason) # why the kernel blocked
print(e.blocking_step) # which step blocked (may be None)
print(e.behavior_id) # correlation back to the Behavior
except ApprovalPending as e:
print(e.request_id) # pending approval request
print(e.fingerprint) # approval fingerprint
print(e.reason)
# Suspend; re-submit the identical call later, it returns allow
# once a human approves. Agent.wait_approval(e.request_id) blocks
# until the human decides.
except GovernanceUnavailable as e:
# /v1/govern unreachable or 5xx. Fail-closed by default.
raise

All three derive from RuntimeError (not from TapPassError) because they are raised by the tool wrapper, not by the Agent chat path. A 4xx from /v1/govern is surfaced as GovernanceBlocked, it indicates a bad payload or auth failure, not a control-plane outage, so fail-open never applies to it.

ApprovalPending fields: request_id, fingerprint, reason, behavior_id. GovernanceBlocked fields: reason, blocking_step, behavior_id.

from tappass import tappass_session, tappass_session_async, current_session
with tappass_session(url="http://localhost:9620", api_key="tp_dev_...") as session:
agent.chat("...") # tagged with session.id
my_tool("...") # govern()-wrapped tools inherit it
async with tappass_session_async(url="...", api_key="tp_dev_...") as session:
await agent.chat("...")

Sessions propagate ambiently to Agent, govern(), and the reporter. Async-safe: each task gets its own copy; threads do not share the ambient session by default.

Pass correlation_id= to override the generated session ID. On exit, the session is popped and the reporter is flushed, even on exception.

current_session() returns the active Session or None.

tappass.guard.bypass_scope() disables @guard enforcement for tests. It only works when TAPPASS_ENV=test and a test runner (pytest/unittest) is detected, defense in depth against accidental production bypass:

import os
os.environ["TAPPASS_ENV"] = "test"
from tappass import bypass_scope
def test_tool_runs():
with bypass_scope():
read_file("/data/report.pdf") # no authorization check

Without TAPPASS_ENV=test, entering the scope raises immediately.