Security-first modular Python wrapper around Microsoft AGT
Project description
zemtik-govern — Governance for LangChain, LangGraph, and Microsoft AGT
Govern every tool call through a fixed pipeline — identity → policy → audit — with a prompt-injection screen folded into the policy seam. Fail-closed by design: any fault during governance blocks the tool and is audited, never silently allowed.
zemtik-govern is a security-first Python wrapper around Microsoft AGT (agent-os-kernel + agentmesh-platform). It is the moat between an autonomous agent and the privileged tools it can call.
What it guarantees
- Deny-by-default. AGT's native evaluator allows when no rule matches;
zemtik-governoverrides that — no matching rule is a deny. A tool is reachable only if a rule names it. - Fail-closed. Any exception in identity or policy is caught, audited as a system denial, and re-raised. The wrapped tool never runs on a governance fault.
- Prompt-injection guard. A poisoned tool argument (
ignore all previous instructions…, role-play hijacks, delimiter smuggling) is denied before the tool runs, even when policy would allow the call. The deny names the offending field; the raw payload is never echoed into logs. - Decision budget. A hung identity or policy seam is bounded by a per-call deadline. A breach is a fail-closed system denial (
DecisionBudgetExceeded), not a leaked allow. - Tamper-evident audit. Every outcome — allow, deny, or system error — is recorded on a Merkle-chained, HMAC-signed trail that verifies independently and survives a process crash. A durable file trail requires an HMAC secret in
$ZEMTIK_AUDIT_SECRET; the in-memory trail does not. - Idempotency. A duplicate
idempotency_keyreplays the cached decision; a key reused with a different payload is an audited conflict, never a false replay. Backing caches are bounded (LRU + TTL) against unbounded growth. - Safe rollout.
shadowmode observes would-denies without enforcing; a per-guardshadowstance lets a new guard run observe-only for one release before it enforces. A kill-switch reverts to a prior governed fallback — never to allow-all.
The API is async throughout (every seam, govern, and proxy) and requires Python 3.11+.
Two ways in: use the LangChain Quick Start if your tools are LangChain/LangGraph; use the AGT-native API to govern any async callable directly.
LangChain Quick Start
pip install 'zemtik-govern[langchain]' # quote the extras for zsh
govern.yaml — allow exactly one tool by name:
mode: strict
audit_sink: memory
rules:
- name: allow-add-numbers
condition:
field: action
operator: eq
value: add_numbers
action: allow
The snippet below wires a stub policy that allows every call — it demonstrates wiring only, not enforcement. For real deny-by-default policy and the injection guard, build the governor from config (GovernanceRegistry.from_config, see the AGT-native API below) and load govern.yaml.
from zemtik_govern.core import ZemtikGovern
from zemtik_govern.identity import AgentRef
from zemtik_govern.langchain import govern_tool
from zemtik_govern.protocols import Decision
from langchain_core.tools import tool
class _Policy:
async def identify(self, s): return AgentRef(did=f"did:x:{s}")
async def evaluate(self, c): return Decision(allowed=True, action=c.action, matched_rule="allow-add-numbers", reason="ok")
async def write(self, e): return "e"
gov = ZemtikGovern(identity=_Policy(), policy=_Policy(), audit=_Policy())
@tool
def add_numbers(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
result = govern_tool(add_numbers, govern=gov).invoke({"a": 3, "b": 4})
print(result) # 7
Debug with full governance logs:
ZEMTIK_DEV=1 python examples/langchain_minimal.py
For a drop-in LangGraph ToolNode replacement, see examples/langgraph_toolnode.py.
Full integration guide: docs/integrations/langchain.md
AGT-native API
Every tool invocation passes identity → policy → audit in fixed order — any fault is a system denial; the guarded tool never runs on a governance error.
pip install zemtik-govern
Copy zemtik.example.yaml to zemtik.yaml and edit it, then wire and call. A request is three inputs: action (the tool being called, what policy matches on), subject (the caller, resolved to a DID by identity), and payload (the arguments, screened by the injection guard).
import asyncio
from zemtik_govern import AGTBoundary, GovernanceConfig, GovernanceContext, GovernanceRegistry
async def main():
config = GovernanceConfig.load("zemtik.yaml")
gov = GovernanceRegistry.from_config(config, AGTBoundary()).build()
ctx = GovernanceContext(action="tool.run", subject="agent-1", payload={"q": "hello"})
decision = await gov.govern(ctx) # raises GovernanceDenied if blocked
asyncio.run(main())
Or wrap a callable so it is governed on every call (inside an async context):
governed_fn = gov.proxy(my_tool, action="tool.run", subject="agent-1")
result = await governed_fn(q="hello")
In strict/enforce mode the injection guard is always on. By default it uses AGT's own vetted detection rules (no config needed); set injection_rules_path only to pin a version or diverge — policies/prompt-injection.yaml is a ready-to-edit snapshot of those defaults. A file audit_sink requires an HMAC secret in $ZEMTIK_AUDIT_SECRET. See zemtik.example.yaml for the full annotated configuration. To watch the injection guard deny a poisoned argument, run scenario S11 in sandbox/qa_demo.py (S14 for the per-guard shadow stance, S16 for the output seam redacting PII; see Sandbox & Demos).
Security model
Operational modes
| Mode | Denials enforced | Policy required | Audit required |
|---|---|---|---|
strict |
Yes | Yes | Yes |
enforce |
Yes | Yes | Yes |
shadow |
No (observe only) | No | Yes |
A new guard can also run observe-only independent of the mode via a per-guard injection: {mode: shadow} or budget: {mode: shadow} stance — useful to watch would-denies for one release before enforcing.
Exceptions
Every failure is a typed, catchable exception carrying a stable .code and .guard, and an .audit_id that correlates back to its audit entry. The two enforcement exceptions raise only in enforcing modes — under shadow (or a per-guard shadow stance) the would-deny is observed and audited but never raised, so the tool runs:
| Exception | Meaning |
|---|---|
GovernanceDenied |
Policy (or the injection guard) denied. Raised in strict/enforce; in shadow it is audited, not raised. Carries the Decision. |
DecisionBudgetExceeded |
A seam outran the decision budget; fail-closed. Raised when budget enforces; under budget: {mode: shadow} the breach is logged, not raised. Carries .limit_seconds / .elapsed_seconds. |
GovernanceError |
A system fault in a seam; the tool was blocked. |
GovernanceNotConfigured |
Insecure startup config; raised at boot, not request time. |
OutputGovernanceDenied |
An output rail blocked a tool's return value (read-classified tool, or an unscreenable/non-JSON return); fail-closed. Carries .rail and .audit_id. Raised in strict/enforce; a shadow rail observes a would-deny instead. |
Output seam (opt-in, proxy() only)
Beyond the input pipeline, an optional post-invocation output seam screens a tool's return value through PII rails — enabled with output_screening: true and only inside proxy() (direct govern()/govern_sync() callers are input-only). The deny shape splits by the tool's tool_io_map classification, because by the time output is screened a write has already executed:
| Audit event | Effect | Caller sees |
|---|---|---|
output_allowed |
clean | the real value |
output_denied_raised |
read-classified hit | OutputGovernanceDenied raised |
output_denied_redacted |
write-classified hit (side effect already ran) | a RedactedOutput sentinel (HIGH-severity audit row) |
RedactedOutput is a poison sentinel: str()/repr() are safe (<output redacted: audit_id=…>) but attribute/item/iter access raises RedactedOutputAccessError. Wrap every governed result in gov.unwrap(result) for one uniform contract — it returns a clean value and raises OutputGovernanceDenied on a redacted one. The rail names itself and its tunable knob in the deny, never the offending value (no-echo). Watch it live with scenario S16 in sandbox/qa_demo.py. See Configuration Reference and the Integration Guide for the rails / tool_io_map config and the observe→enforce rollout.
Documentation
- LangChain Integration Guide — govern_tool, GovernedToolNode, LangSmith, error reference
- Architecture — three-seam design, idempotency, fail-closed guarantee, deny-by-default moat
- Integration Guide — quickstart, usage patterns, custom seams, error reference
- Configuration Reference — YAML fields, mode matrix, env vars, startup validation
- Operations Guide — deployment checklist, durable audit, kill-switch, shadow rollout, monitoring
- API Reference — full public API (classes, protocols, exceptions)
- Sandbox & Demos — runnable demos: three-seam scenarios, the injection battery, audit forensics, staged shadow → enforce cutover, a real
gpt-5.4-nanoagent governed end to end
Sandbox & Demos
Runnable demos live in sandbox/ and exercise the real pipeline against the live AGT stack:
source .venv/bin/activate
# Three-seam scenarios S1–S16: allow, deny, fail-closed, idempotency, the
# injection guard, the decision budget, error codes/audit_id, per-guard shadow,
# and the output seam redacting PII (S16)
ZEMTIK_AUDIT_SECRET=qa-test-secret python sandbox/qa_demo.py
# Audit trail: verify Merkle/HMAC, extract proofs, detect tampering
ZEMTIK_AUDIT_SECRET=audit-secret python sandbox/auditor.py
# Staged dogfood cutover: shadow -> enforce, kill-switch revert, audit integrity (no API key)
ZEMTIK_AUDIT_SECRET=dogfood-secret python sandbox/dogfood_cutover.py
# A real gpt-5.4-nano agent governed end to end, plus a 15-prompt injection
# battery and deterministic module probes — including the output seam redacting
# PII from a tool's return (needs [langchain,openai] + an OpenAI key)
uv pip install -e ".[dev,langchain,openai]"
cp .env.example .env # then set OPENAI_API_KEY in .env (gitignored)
python sandbox/e2e_openai_governed.py
See docs/sandbox.md for what each demo proves and its outputs.
Development
source .venv/bin/activate
pytest
ruff check src/
See CONTRIBUTING.md for the full dev setup and three-seam contract.
Status: v0.4.0.0 — see CHANGELOG.md. License: Apache-2.0.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zemtik_govern-0.4.0.0.tar.gz.
File metadata
- Download URL: zemtik_govern-0.4.0.0.tar.gz
- Upload date:
- Size: 406.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4489bd66760fec488e4337556151a0d441159884b68d820837a83541d0fe4a61
|
|
| MD5 |
4ad8d8ab4ba7ddae3c59dc5862546f8f
|
|
| BLAKE2b-256 |
43133e1c288212d81cafd7a96f5875d3e61833087488e7ebf580013103d0f359
|
Provenance
The following attestation bundles were made for zemtik_govern-0.4.0.0.tar.gz:
Publisher:
publish.yml on dacarva/zemtik-govern
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zemtik_govern-0.4.0.0.tar.gz -
Subject digest:
4489bd66760fec488e4337556151a0d441159884b68d820837a83541d0fe4a61 - Sigstore transparency entry: 1961881377
- Sigstore integration time:
-
Permalink:
dacarva/zemtik-govern@5f2a6829954fe805f797181a04442203e4c519c4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/dacarva
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5f2a6829954fe805f797181a04442203e4c519c4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file zemtik_govern-0.4.0.0-py3-none-any.whl.
File metadata
- Download URL: zemtik_govern-0.4.0.0-py3-none-any.whl
- Upload date:
- Size: 86.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f0298c44a22a1c2983b1073304b5fbc8ce159d27eefe8a407722f13b2bc87fa
|
|
| MD5 |
6c40b4263be41afb2bb95c4187ac3294
|
|
| BLAKE2b-256 |
03d9cd2506f933f7a413fc2065cc4666cd45f3fda7b1f31862ad2a7feb3feb30
|
Provenance
The following attestation bundles were made for zemtik_govern-0.4.0.0-py3-none-any.whl:
Publisher:
publish.yml on dacarva/zemtik-govern
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zemtik_govern-0.4.0.0-py3-none-any.whl -
Subject digest:
8f0298c44a22a1c2983b1073304b5fbc8ce159d27eefe8a407722f13b2bc87fa - Sigstore transparency entry: 1961881456
- Sigstore integration time:
-
Permalink:
dacarva/zemtik-govern@5f2a6829954fe805f797181a04442203e4c519c4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/dacarva
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5f2a6829954fe805f797181a04442203e4c519c4 -
Trigger Event:
push
-
Statement type: