Policy enforcement
Deny-by-default. Every tool call returns a typed Decision(allow, deny, or approval-required), evaluated against the caller's role at call time.
One agent, many users, each gated by their own role. Hexgate carries per-request user identity through every tool call, with policy enforced in-process from a signed WASM bundle. Fine-grained control with zero added latency on the critical path.
Wraps the agent you already built
Past the gate, each verdict streams to an append-only log: the caller's role, the tool, the outcome, and the exact constraint behind it.
The capability you give an agent is the capability it can be jailbroken into using. Hexgate sits at that boundary and turns it into four primitives you can edit, version, and audit.
Deny-by-default. Every tool call returns a typed Decision(allow, deny, or approval-required), evaluated against the caller's role at call time.
The signed WASM bundle is fetched once per runand enforced in-process. No security service on the hot path, no round-trip per decision. Fast by design, verified before it's trusted.
Biscuit tokens carry who is calling; role policies decide whatthey can do. One wrapped agent serves every user: same code, different effective permissions per request. Other governance toolkits gate the agent. Hexgate gates the user, through the agent.
Every decision streams to the audit log: who acted, which tool, the verdict, and the exact constraint that allowed or blocked it. When someone asks why a call went through, you can show them the line.
No rewrite, no config object. Set a key, wrap the runner, and the same agent code gates every tool boundary.
from hexgate.adapters.openai import HexgateRunner
from hexgate.runtime import User
# picks up HEXGATE_KEY from env, no rewrite
runner = HexgateRunner()
await runner.run(
my_agent,
"refund order 30",
user=User(user_id="alice", role="billing"),
)
# ↳ every tool call now routes through policyversion: 1
inherits: [read_only]
default_policy:
mode: deny
tools:
refund_order:
mode: allow
constraints:
- args.amount <= 500
- args.currency == "USD"
wire_transfer:
mode: approval_required✓ Identical decisions in dev (in-process) and prod (signed WASM), proven by a parity test suite.
OpenAI, LangChain, Google ADK, or Pydantic AI: wrap it once. Your original object is left untouched.
Each tool invocation resolves the caller's role and returns allow, deny, or approval-required. Denials come back as tool results the model can react to, so a blocked call doesn't abort the run.
Decisions stream to the log with the exact constraint behind each verdict. Hot-reload policy without a restart.
The short version of how Hexgate behaves in a real codebase.
No. Hexgate ships adapters that wrap an existing OpenAI Agents, LangChain / LangGraph, Google ADK, or Pydantic AI agent without touching its logic. Swap your runner for HexgateRunner (or call wrap_langchain_agent / wrap_pydantic_agent) once. Your original agent object is left intact; the wrapper holds the policy and gates every tool the agent can invoke.
No per-decision round-trip. Policy is evaluated in-process, either by the default pydantic engine or in production by a compiled WASM bundle run via wasmtime. The bundle is fetched once and refreshed only at turn boundaries with an ETag / 304 check, so individual decide() calls never leave the process.
A denial isn't a crash. The tool returns a [policy_denied] (or [approval_required]) marker that the model sees as the tool result, so the agent can recover or try a fallback instead of aborting the run. On Pydantic AI it surfaces as a ModelRetry; on LangChain as a structured {ok: false} result.
Mark a tool approval_required in policy, then pass an approval_handler when you wrap: True (auto-approve), False (auto-deny), or a sync/async (action, context) -> bool callback that inspects the specific call. hexgate chat prompts the terminal, hexgate serve auto-approves, and native code does whatever you wire.
Identity and rules are decoupled. A per-request User context manager carries who is calling (user_id, role, session_id, optional ttl) as a signed biscuit token; role policy files decide whatthat role can do. Role is resolved at call time from a contextvar, so a single wrapped agent serves many users concurrently without seeing each other's policies.
Unlike governance toolkits that key policy on agent_id alone, Hexgate threads the end-user identity through every decision. The same agent code runs with different effective permissions depending on which user invoked it. See the full breakdown in Hexgate vs Microsoft Agent Governance Toolkit.
A policy.yaml is deny-by-default with a tools map; each tool gets a mode (allow / deny / approval_required) and optional constraints like args.amount <= 500. Operators are ==, !=, <, <=, >, >=, in, not in, all ANDed.
The same constraint strings compile to OPA Rego for the WASM engine and run in-process for pydantic. A parity test suite proves both produce identical decisions.
Bundles are signed. The manifest carries a SHA-256 of every artifact (including the wasm_hash) plus a detached Ed25519 signature over that manifest. The hashes authenticate the files; the signature authenticates the manifest. Set HEXGATE_BUNDLE_REQUIRE_SIGNATURE=true to refuse anything unsigned or unverifiable. The signing key is the same root that signs your biscuit tokens.
The SDK runs standalone: YAML on disk, in-process enforcement, no Docker or browser. The optional platform (a FastAPI control plane + React dashboard) adds browser policy editing, mintable tokens, a live Playground decision stream, and an append-only audit log in ClickHouse. Edit policy in the UI and the next turn picks it up.
Install the SDK and gate your first agent in minutes, spin it up on Hexgate Cloud, or book a walkthrough of the platform, audit log, and signed-bundle workflow.