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) |
Audit mode
Section titled “Audit mode”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.
Enforce mode
Section titled “Enforce mode”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) |
Enforce-mode exceptions
Section titled “Enforce-mode exceptions”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 Behaviorexcept 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. raiseAll 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.
Ambient sessions: tappass_session
Section titled “Ambient sessions: tappass_session”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.
Testing: bypass_scope
Section titled “Testing: bypass_scope”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 osos.environ["TAPPASS_ENV"] = "test"
from tappass import bypass_scope
def test_tool_runs(): with bypass_scope(): read_file("/data/report.pdf") # no authorization checkWithout TAPPASS_ENV=test, entering the scope raises immediately.
Next steps
Section titled “Next steps”- Agent client — chat, streaming, and the
GovernanceDecisiontree - Resilience — what happens when TapPass is unreachable
- SDK environment variables —
TAPPASS_URL,TAPPASS_FAIL_OPEN, and friends - Your first agent — end-to-end walkthrough