Developers
One decision, before the tool runs. Put it where the action already happens.
GovernorAI asks one question at one endpoint — may this call proceed, with these arguments, right now — and answers allow, deny or pause. Wrap the function, route through the MCP proxy, or insert at a gateway you already operate. Keep your tools, your frameworks and your policy authority; stop rebuilding the same approval workflow in every agent.
Same tool. Different decision.
The agent can call payments.refund. Should this refund proceed?
Nothing has been decided. The target has not been called.
One cloud. Still many places an agent can act.
An AWS-only estate with a few Python agents, an MCP server, some internal APIs and three teams shipping on their own cadence already has five places a refund can be issued. Native platform policy covers the ones it can see. The question is whether every team should build and maintain its own business-rule, approval and evidence workflow for the rest.
Stop rebuilding the same approval logic.
Refund limits, production-change rules and review requirements get one owner and one version, instead of a different check embedded in every agent.
Ship within boundaries you can read.
Teams keep their tools and frameworks. A policy change has its own review path and release, rather than waiting on every agent's next deploy.
Debug the decision, not just the request.
Join the attempted action to its policy reason, its approval state and its dispatch. The target's outcome is recorded separately: an allow is permission, not a receipt.
Useful on one platform. Reusable across more as the action paths multiply. Where a platform's native controls already answer one of these, keep them; GovernorAI sits beside them, not in place of them.
Choose the seam. The decision is the same at every one.
Pick the path first, then take its setup, identity mapping and failure contract. Each path builds the same canonical execute request and asks the same decision core; none of them re-implement policy, data controls, approvals, kill switches or evidence.
Wrap the function
You own the callable that changes a business system. The SDK asks first and either invokes it or raises.
What changes: a decorator or three lines. Your credentials and control flow stay yours. your toolsRoute through the MCP proxy
The agent reaches its tools through an HTTP MCP server. Point the client at the proxy; tools/call is governed.
Insert at your gateway
The request already crosses Envoy, NGINX, Traefik, an API gateway or an LLM gateway. An adapter asks from there.
What changes: adapter configuration. Zero lines in the agent. your hostsRun the sidecar
The engine runs beside the agent on the same host and evaluates the policy files mounted beside it, plus Rego and kill switches from a signed bundle. No round trip per action.
What changes: a sidecar on the pod or host. Zero lines in the agent.Terraform carries the reviewed change; the seams above carry the decision. Declaring a policy is not intercepting a call, so every assignment is paired with a runtime integration and a test that proves it blocks.
Inspect the boundary before choosing a path: what reaches the check, and what to validate
| Path | What reaches the check | Verdicts it can apply | What to validate before production |
|---|---|---|---|
| Function wrapper | Explicitly wrapped invocations and the arguments you pass | allow · deny · pause · shaped args | Deny and pause prevent the function body. An unwrapped function or an alternate credential path is outside this wrapper. |
| MCP proxy | Routed tools/call requests: tool name, arguments, per-call identity headers | allow · deny · pause · shaped args | Identity header handling, JSON-RPC error mapping, upstream delivery. Handshake and discovery are passed through and are not tool execution. |
| Envoy ext_proc | Headers and the buffered request body | allow · deny · pause · body mutation | Buffering limits, argument extraction, payload rewrite, and failure_mode_allow: false on the filter itself. |
| Forward-auth (NGINX, Traefik, APIM, Lambda) | Only the context you explicitly forward in headers | allow · deny · pause | A status-based auth check is not a body inspector or a rewriting proxy. Derive identity at the gateway and strip caller-supplied X-Governor-* headers. |
| LLM gateway hook (Portkey, LiteLLM) | The model request or response passing through the hook | allow · deny · pause · transformed messages | A model-call check does not, by itself, intercept the external tool's eventual execution. Pair it with a tool-boundary seam. |
| Sidecar | Whatever the adapter or SDK beside it sends; evaluated against its mounted policy files and the signed Rego and kill-switch bundle | allow · deny · pause · shaped args | Bundle signature and version, TTL behavior when the control plane is unreachable, evidence replay on reconnect. |
| Cloud / SaaS connector | Depends on the specific connection and action path | Varies by platform | Distinguish discovery, before-dispatch, before-commit and after-event controls. An after-event connector does not prevent the original action. |
These are integration boundaries, not a claim of equal capability on every platform. The per-platform matrix is on Runtime Enforcement.
Own the function. Govern the invocation.
Where there is no seam to insert at — a plain agent, a script that orchestrates tool calls, a job that writes to a system of record — the SDK is a thin client over one endpoint. It calls execute, then either invokes your function or raises. It does not proxy your traffic, hold your provider keys, or sit between your code and the tool.
Install the pinned package in a clean virtualenv. Set GOVERNOR_URL, GOVERNOR_API_KEY and GOVERNOR_ORG_ID. All three come with access: the console shows them under API keys.
pip install governor-sdk==0.4.*A sandbox namespace and a policy that matches the decorated tool name and its exported argument shape.
governor policies create -f refund-approval.yamlAssert the stub never runs on a policy deny, on an approval requirement, and on a gateway error. Then move the same boundary in front of the real tool.
pytest tests/test_boundary.pyOn deny, the wrapped function is never invoked.
A denied call raises before your function runs, carrying the policy reason and the rule that produced it, so the write to the system of record does not happen and then get reported. A paused call raises with the approval, and the function is likewise not called. A gateway error raises too. Allow returns whatever your function returns, unchanged.
Pause is a branch you handle, not an error you swallow.
The approval exception carries approval_id and approval_url for your own UI. A reviewer's approval is recorded, but it does not release the call: re-running it is a new request that the approval does not cover, and the ledger joins both attempts through session_id.
A wrapper governs the calls that go through it. It does not make an unwrapped function or an alternate credential path unreachable; that is what the gateway and MCP seams are for.
- pip install governor-sdk
- Python 3.9+. Sync and asyncio. Decorator, one-shot wrap, low-level client.
- TypeScript SDK
- TypeScript-first, Node 18+ and edge runtimes. The same three shapes. The TypeScript SDK ships with design-partner access. Request a briefing
- Java · C# · Ruby
- Generated from the OpenAPI spec. A generated client is an HTTP client, not a framework interceptor: you enforce the verdict before you call the tool.
from governor import (
governed_fn, GovernorAIDenied,
GovernorAIApprovalRequired, GovernorAIError,
)
@governed_fn("payments.refund", agent_id="billing-agent", namespace="sandbox")
def refund(amount: int, currency: str) -> dict:
# local stub. no payment-provider call.
return {"simulated": True, "amount": amount}
try:
print(refund(amount=20000, currency="USD"))
except GovernorAIApprovalRequired as e:
print("needs review:", e.approval_url)
except GovernorAIDenied as e:
print("denied:", e, e.rule_id) # ← the expected first result
except GovernorAIError as e:
raise SystemExit(f"no usable decision; nothing dispatched: {e}")from governor import govern_call, GovernorAIClient, GovernorAIDenied
def create_case(customer_id: str) -> dict:
return {"case_id": "CASE-001"} # your implementation, your credentials
try:
result = govern_call("crm.create_case", {"customer_id": "123"},
agent_id="support-agent", fn=lambda: create_case("123"))
except GovernorAIDenied as e:
log.warning("denied: %s (rule %s)", e, e.rule_id)
# for anything that is not a Python function — a CLI invocation, a shell exec, a database write
client = GovernorAIClient() # reads GOVERNOR_URL / GOVERNOR_API_KEY / GOVERNOR_ORG_ID
verdict = client.execute(tool="payments.refund", args={"amount": 8200},
agent_id="billing-agent", session_id="conv-4f2a", namespace="production")
run_refund(amount=8200) # reached only on allow: deny and pause raise
log.info("allowed: %s", verdict["action_id"]) # the join to the audit rowimport pytest
from unittest.mock import MagicMock
from governor import govern_call, GovernorAIDenied, GovernorAIApprovalRequired, GovernorAIError
def test_deny_never_invokes(sandbox): # sandbox fixture: policy refund-approval, namespace sandbox
target = MagicMock()
with pytest.raises(GovernorAIDenied) as e:
govern_call("payments.refund", {"amount": 20000}, agent_id="billing-agent", fn=target)
target.assert_not_called()
assert e.value.rule_id == "refund_cap"
def test_pause_never_invokes(sandbox):
target = MagicMock()
with pytest.raises(GovernorAIApprovalRequired):
govern_call("payments.refund", {"amount": 8200}, agent_id="billing-agent", fn=target)
target.assert_not_called()
def test_gateway_down_fails_closed(sandbox, unreachable_gateway):
target = MagicMock()
with pytest.raises(GovernorAIError):
govern_call("payments.refund", {"amount": 250}, agent_id="billing-agent", fn=target)
target.assert_not_called()// The TypeScript SDK ships with design-partner access: GovernorAIClient, PolicyDeniedError, ApprovalRequiredError
const client = new GovernorAIClient(process.env.GOVERNOR_URL, { apiKey: process.env.GOVERNOR_API_KEY });
const session = client.newSession("billing-agent", "production");
async function refund(amount: number, customerId: string) {
await session.execute("payments.refund", { amount, customerId }); // throws before the refund on deny or pause
return stripe.refunds.create({ amount, customer: customerId });
}
try {
await refund(8200, "cus_8d1");
} catch (e) {
if (e instanceof ApprovalRequiredError) notifyReviewer(e.approvalUrl);
else if (e instanceof PolicyDeniedError) log.warn(e.message, e.ruleId);
}Deny one tool call, and read the decision.
The shortest path to a governed action: point an MCP client at the transparent proxy, write one rule, watch the call stop, find the record it left, then prove it fails closed. Nothing is installed inside the agent. tools/call is governed; initialize, tools/list, ping and notifications pass through untouched, so discovery and handshake behave exactly as before.
Container image or static binary for linux amd64/arm64. One proxy fronts one upstream MCP server.
--build-arg BINARY=governor-mcp-proxyConfigured entirely by environment. GOVERNOR_MCP_UPSTREAM_URL is the only required value.
:9003Change the MCP client's server URL to the proxy. Nothing else in the client changes.
http://localhost:9003One tool, denied unconditionally — narrow enough to trigger on purpose.
governor policies create -f deny-refund.yamlJSON-RPC error -32002 comes back; the event row carries rule_id and action_id.
GET /api/v1/events?decision=denyStop the policy engine, call again. Denied, flagged fail_closed. No knob turns that off.
governor compliance audit-verify# the image, built from your GovernorAI release
docker build -f deployments/gateway-insertion/Dockerfile \
--build-arg BINARY=governor-mcp-proxy -t governor-mcp-proxy .
docker run -p 9003:9003 \
-e GOVERNOR_MCP_UPSTREAM_URL=https://mcp.internal/servers/payments \
-e GOVERNOR_MCP_ID=mcp-payments \
-e GOVERNOR_MCP_NAMESPACE=mcp-quickstart \
-e GOVERNOR_API_KEY=gov_… \
-e GOVERNOR_GATEWAY_URL=http://host.docker.internal:8080 \
governor-mcp-proxy
# per-call identity travels in request headers and overrides the defaults:
# x-governor-agent-id x-governor-principal-id x-governor-session-id (falls back to Mcp-Session-Id)
# the proxy reads these headers as sent: set them at a trusted hop in front of it, and drop any the client supplies.id: mcp-quickstart
name: mcp-quickstart
namespace: mcp-quickstart # must equal GOVERNOR_MCP_NAMESPACE
governance_mode: enforcement
fail_closed: true
version: 1
rules:
- id: deny_refund_payment
priority: 1
match: { tool: "refund_payment" }
action: denyHTTP/1.1 200 OK
x-governor-decision: deny
{ "jsonrpc": "2.0", "id": "abc",
"error": { "code": -32002, "message": "denied by policy",
"data": { "reason": "Matched rule: deny_refund_payment" } } }
# a pause is -32001 and carries approval_id and approval_url.
# a redact verdict returns no error: the proxy rewrites params.arguments and forwards the shaped values.The first success is a verified deny.
For a governance product, a 200 is not the proof. The proof is that the upstream MCP server never received refund_payment, that the event row says why, and that the same call is still denied when the policy engine is down.
- Fail-closed, with no setting
- Transport error, timeout, any non-2xx (even one whose body says allow), unparseable body, body over 1 MiB, a
tools/callwith no tool name, or one inside a JSON-RPC batch: all resolve to-32002and are never forwarded.GOVERNOR_MCP_DECISION_TIMEOUT(5s) only bounds how long a hung gateway takes to become a deny. - Events carry a digest of the arguments
- The event row carries
args_hash, a SHA-256 of the canonical arguments. An approval request keeps the arguments a person is asked to approve. Shaped values reach the upstream; originals do not. - Already have an MCP gateway?
- The proxy chains inside it. If you run an Envoy-family gateway,
ext_procis the shorter route in production.
If a gateway already sees the call, that is where the decision goes.
Most agent traffic already crosses something you operate — an Envoy-family gateway, an NGINX or Traefik hop, an API gateway, or an LLM gateway such as Portkey or LiteLLM. Each adapter is a thin protocol translator: it parses the tool call off the wire, builds the canonical execute request and asks for the verdict. Each ships as a signed static binary and a container image for linux amd64 and arm64.
| Adapter | Binary · port | Inserts into | Verdict → action on the wire |
|---|---|---|---|
Envoy ext_proc | governor-extproc :9002 gRPC | Envoy Gateway, Istio, Gloo, Envoy AI Gateway, agentgateway | allow → CONTINUE · redact → body mutation · pause → 202 · deny → 403 |
| HTTP forward-auth | governor-checkgw :9004 HTTP | NGINX auth_request, Traefik ForwardAuth, AWS Lambda authorizer, Apigee, Azure APIM | allow → 200 · pause / deny → 403. Headers only: no body inspection, no rewrite on this seam. |
| LLM-content coexistence | governor-checkgw :9004 HTTP | Portkey BYO-guardrail webhook; LiteLLM CustomGuardrail | Governs the model call. allow → verdict:true · redact → transformed messages · deny, pause or error → 200 with verdict:false |
Every binary reads GOVERNOR_GATEWAY_URL (default http://localhost:8080), GOVERNOR_API_KEY and GOVERNOR_ORG_ID, and bounds each decision at 5 seconds by default (GOVERNOR_DECISION_TIMEOUT_MS on governor-extproc; GOVERNOR_CHECKGW_DECISION_TIMEOUT and GOVERNOR_MCP_DECISION_TIMEOUT on the others).
- name: envoy.filters.http.ext_proc
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
failure_mode_allow: false # the one line that decides whether an unreachable processor stops the call
allow_mode_override: false
processing_mode:
request_header_mode: SEND
request_body_mode: BUFFERED # arguments come from the body, not from caller headers
message_timeout: 5s
grpc_service:
envoy_grpc: { cluster_name: governor-extproc } # :9002
# identity: the adapter reads x-governor-agent-id, -org-id and -principal-id from the request.
# set them in Envoy from the JWT it validated (jwt_authn), and strip any the caller sent. guardrails:
- guardrail_name: governorai-pre
litellm_params:
guardrail: governor_guardrail.GovernorAIGuardrail
mode: pre_call # before the provider is called; the seam that also covers streaming
api_base: os.environ/GOVERNOR_LLMGUARD_URL # governor-checkgw, e.g. http://governor-checkgw:9004
fail_open: false # block when the check cannot answer # a Bring Your Own Guardrail webhook check, on the before- and after-request hooks
url: https://governor-checkgw.internal:9004/v1/portkey/guardrail
headers: { x-governor-org-id: acme, x-governor-agent-id: support-agent }
on fail: deny # Portkey fails open on a webhook timeout unless the check denies
# checkgw answers inside 2.5 s (GOVERNOR_LLMGUARD_DEADLINE), under Portkey's 3 s webhook timeout NGINX, Traefik and other HTTP forward-auth hops are set up with an engineer during a briefing while we finish verification of that install path. Request a briefing
The decision on the host, from the policy beside it.
Where a network round trip per action is the wrong price — high-frequency tool loops, runtimes far from the control plane, environments that cannot call out — the engine runs beside the agent. It evaluates the policy files mounted in its pod, read when it starts, plus the Rego and kill switches from a bundle the control plane signs; the sidecar pulls that bundle, verifies the signature, refuses an older one, and decides in process. Decisions are batched to the ledger with the policy and rule that matched, when one did.
- Bundle
- The Rego and kill-switch bundle, signed in the control plane. The sidecar refuses an unsigned or mis-signed bundle and keeps the last verified one.
- Decision
- In process, deterministic, no inference in the hot path. The engine cost is measured on the benchmark; the hop around it is what you will measure in a deployment.
- Control plane unreachable
- Decisions continue on the mounted policy files and the last verified bundle. When the bundle expires, a gateway that takes its kill switches from it denies every call. Nothing in the sidecar turns that off.
- Evidence
- With the signed bundle configured, written to a local log and sent to the control plane when it is reachable again.
A decision is one fact. Execution is another.
Build against four facts that are recorded separately: the policy verdict, the human authority, the dispatch, and the target's result. Make the next step obvious to your code and the reason legible to your operators.
Allow is permission, not completion.
Proceed only at the governed boundary. Record delivery and the target's result separately; a missing response is unknown, not failed.
Pause means do not dispatch.
Surface the approval, keep the context. A reviewer's decision is recorded as authority; it does not invoke the tool.
Deny stops this governed action.
The response carries the reason and the rule. A policy refusal and a fail-closed denial are told apart by fail_closed, so an operator fixes the right problem.
An approval is a decision, not a release.
A signed-in reviewer approves or rejects one held request, and a known requester cannot approve their own. The approval does not release the call: re-running it is a new request that the approval does not cover. The one exception is a run-loop maker-checker: a distinct reviewer's attestation lets later actions in that loop's window pass.
curl -X POST $GOVERNOR_GATEWAY_URL/api/v1/gateway/execute \
-H "Authorization: Bearer $GOVERNOR_API_KEY" -H "X-Org-ID: $GOVERNOR_ORG_ID" \
-H "Content-Type: application/json" \
-d '{ "tool": "payments.refund",
"args": { "amount": 8200, "customer_id": "cus_8d1" },
"agent_id": "billing-agent", "session_id": "conv-4f2a", "namespace": "production" }' verdict = client.execute(tool="payments.refund", args={"amount": 8200, "customer_id": "cus_8d1"},
agent_id="billing-agent", session_id="conv-4f2a", namespace="production") const verdict = await client.execute("payments.refund", { amount: 8200, customerId: "cus_8d1" },
"billing-agent", { sessionId: "conv-4f2a", namespace: "production" }); { "decision": "allow", "action_id": "act_01JA7M3Q9K", "policy_id": "refund-approval", "latency_ms": 1 }
# permission. dispatch and the target's result are yours to record — see events for the join on action_id. { "decision": "allow", "action_id": "act_01JA7M3Q9K", "rule_id": "redact_pii_in_notes",
"args_constrained": true,
"shaped_args": { "amount": 8200, "customer_id": "cus_8d1", "note": "[REDACTED]" } }
# returned to a caller that executes the tool itself ("inbound_execute": true).
# args_constrained is the authoritative signal. forward shaped_args, never the originals. HTTP/1.1 202 Accepted
{ "decision": "pause", "action_id": "act_01JA7M3Q9K",
"approval_id": "appr_7c21", "approval_url": "/api/v1/approval-workflows/requests/appr_7c21",
"approval_workflow_id": "finance-managers" }
# your function is not called. an approval records the reviewer's decision; it does not release this call. { "decision": "deny", "action_id": "act_01JA7M3Q9K", "rule_id": "refund_cap", "policy_id": "refund-approval",
"reason": "Matched rule: refund_cap" } HTTP/1.1 403 Forbidden # governor-checkgw, when the gateway did not answer
X-Governor-Decision: deny
X-Governor-Fail-Closed: true
X-Governor-Reason: gateway unreachable (fail-closed)
# synthesized by the adapter because the gateway did not answer; the SDKs raise GovernorAIError instead.
# same block, different log line. action_id, the join to the audit row.One policy object. Native rules and Rego. A diff for every change.
The policy is the center of GovernorAI: the same object is authored, tested, versioned, assigned, enforced and reported. Write it in GovernorAI's native rules or in Rego for the OPA you already run, and mix the two per policy; CEL authoring is planned. Whichever engine evaluates it, the decision record cites the same policy.
id: refund-approval
name: refund-approval
namespace: production
governance_mode: enforcement # audit_only | shadow | enforcement | learning
fail_closed: true
rules:
- id: refund_cap
priority: 10 # lower runs first
match:
tool: payments.refund
condition: { field: args.amount, operator: ">", value: 15000 }
action: deny
- id: refund_over_5k
priority: 20
match:
tool: payments.refund
condition: { field: args.amount, operator: ">", value: 5000 }
action: require_approval # held for the finance-managers approval workflowpackage governor.decision
import future.keywords.if
default allow := false
default deny := false
default require_approval := false
default reason := ""
default rule_id := ""
deny if { input.action.tool == "payments.refund"; input.action.args.amount > 15000 }
require_approval if { input.action.tool == "payments.refund"; input.action.args.amount > 5000; not deny }
allow if { input.action.tool == "payments.refund"; not deny; not require_approval }
reason := "refunds above 15000 are not permitted" if deny
reason := "refunds over 5000 require a human decision" if require_approval
rule_id := "refund_cap" if deny
rule_id := "refund_over_5k" if require_approval
decision := {"allow": allow, "deny": deny, "require_approval": require_approval,
"reason": reason, "rule_id": rule_id}
# point the gateway at your OPA server, or let GovernorAI host the bundle.
# either way the decision is read at data.governor.decision.decision.From a draft to a decision record
Start from a template for the consequential actions — refunds, deletes, outbound email, IAM changes — or from a blank file.
governor policies create -f refund-approval.yamlReplay recorded actions against the draft. The output is a diff of verdicts: what would newly pause, deny or allow.
governor batch-simulate -i recorded.yaml -p refund-approvalA pull request, a reviewer, a plan. The version increments on merge; decision records do not yet cite it.
terraform planObserve mode first: decisions are logged, nothing is blocked, existing protections stay active. Flip to enforce per namespace or per agent.
governance_mode: shadowThe Policy Graph shows which agents, seams and controls the policy touches, and every decision it produced. Rollback is a version pin.
governor policies rollback refund-approval --version 6Let the first draft come from what the agent actually did.
Run a new agent in observe mode for a few days. GovernorAI drafts a policy from the record — the tools it called, the argument ranges, the hours, the systems it touched — and hands the draft to a human to edit and approve. It lands as a file in your repository like any other.
A kill switch with an author and a diff is a different artifact from one with a timestamp.
When policies, assignments, approval workflows and kill switches are Terraform resources, the question an auditor asks — who changed this control, when, and who approved it — is answered by version control rather than a screenshot. The separation of duties is the one your pipeline already enforces.
- governor_policy
- The rule set:
name,description,enabledand itsrules, with a computedversion. - governor_namespace
- The grouping agents belong to — production, staging.
- governor_policy_assignment
- Binds a policy to a namespace or one agent; the agent binding wins.
- governor_agent
- Registration: name, namespace, provider, tags, capabilities.
- governor_approval_workflow
- The human gate a pause routes to; approvers, a timeout, and whether a timeout denies.
- governor_kill_switch
- Agent, reason, and who activated it.
- governor_automation_config
- Discovery, lifecycle, policy and onboarding modes.
- governor_integration
- What GovernorAI reads from and writes evidence to.
provider "governor" {
server_url = var.governor_url
api_key = var.governor_api_key
}
resource "governor_namespace" "production" {
name = "production"
description = "Agents that can act on systems of record"
}
resource "governor_policy" "refund_approval" {
name = "refund-approval"
description = "Refunds over 5,000 need finance; over 15,000 are refused" # reviewed in this PR
enabled = true
}
resource "governor_approval_workflow" "finance_managers" {
name = "finance-managers"
description = "Finance review for large refunds"
policy_id = governor_policy.refund_approval.id
approvers = ["group:finance-managers"]
timeout_minutes = 240
auto_deny_on_timeout = true
}
resource "governor_policy_assignment" "refunds_in_prod" {
policy_id = governor_policy.refund_approval.id
namespace_id = governor_namespace.production.id
}Don't just test the happy path. Test what cannot run.
Make "the target was never called" a test assertion. Exercise the denied, pending, invalidated, unreachable and duplicate cases before connecting a consequential tool.
action_id appears on the event row.rule_id is the one you expect.approval_url is present; expiry is set.fail_closed: true; nothing dispatched.The operational questions worth answering before rollout
Identity and tenant context
Timeouts, outages and local policy
Retries and uncertain target outcomes
action_id for this) and reconcile before retrying. The SDK's retry policy applies to the decision call, never to your function.Async, streaming and payload changes
shaped_args and args_constrained says so. On the LLM-gateway seam streaming is buffered by the gateway; what may already have been released is documented per adapter.Shadow rollout and rollback
Logs, privacy and evidence availability
compliance audit-verify checks the hash chain under a stated custody model; integrity of the record is what it proves, not that the target performed the action.DNS, TLS, connection refused.
No answer inside the decision timeout, 5 seconds by default.
Even one whose body says allow.
Not a verdict the client can read, or over 1 MiB.
- uses: SentinelLayer/GovernorAI/.github/actions/assurance-gate@main
with:
command: gate
server-url: ${{ vars.GOVERNOR_URL }}
api-key: ${{ secrets.GOVERNOR_API_KEY }}
image-digest: ${{ steps.build.outputs.digest }}
# register → assess → gate → deploy-begin → [your deployment] → promote0 pass, or risk accepted
1 blocked — a verdict about the candidate
2 usage or malformed configuration — not a verdict about the agent
3 control plane unreachable after the retry budget — never 0, even with --allow-degraded
4 authentication or authorization failure — no flag makes this tolerableMeasure the whole boundary. Not just the evaluator.
The number that matters to you is the incremental delay around your tool call on the seam you chose, with the policy, detectors, load and evidence settings you will actually run. That is five hops, and the decision is the smallest of them.
The engine evaluates policy in process, beside the agent. The microbenchmark figures, with their method and what they exclude, are on the benchmark page.
What it tells you: anything you can measure in a deployment is the hop around the decision, not the decision. What it does not tell you: end-to-end sidecar p50/p95/p99, which are measured separately and published on the benchmark page as they land.
| Seam | p50 | p95 | p99 | Conditions |
|---|---|---|---|---|
| Sidecar | in measurement | EKS, c7g, 50-rule policy, evidence batched | ||
| MCP proxy | in measurement | same host as control plane | ||
| Envoy ext_proc | in measurement | buffered body ≤ 64 KiB | ||
| SDK → control plane | in measurement | same region, TLS | ||
Report governance overhead, target latency and human review time separately. A configured timeout is a bound on failure, not a latency result.
A verdict you can act on, and a record you can query.
Every governed call returns a decision with the reason and the rule that produced it, and leaves an audit row behind. The same record is reachable from more than one direction, because the person debugging an agent and the person answering an audit request are rarely the same person and never want the same interface.
The verdict
decision, reason, rule_id, policy_id and action_id; approval_url on a pause; shaped_args and args_constrained where the seam can carry a rewritten payload.
POST /api/v1/gateway/executeLineage
Every audit row carries session_id. Pass a stable one per conversation and the full tool sequence for a run replays in order.
GET /api/v1/events?session_id=GraphQL
Published schema and playground. agents, policies, events filterable by agent, decision and time range, approvals, alerts, automationLogs.
/api/v1/graphqlWebhooks
policy.decision, killswitch.activated, agent.registered. HMAC-SHA256 signed, idempotency key, dead-letter and replay.
X-Governor-Signature-256OpenAPI
The REST surface as a spec with a Swagger UI. HTTP clients for Java, C# and Ruby are generated from it; they return the verdict, you enforce it.
/api/v1/openapi.yamlCLI
One binary, governor: agents, policies, guardrails, kill switches, approvals, lifecycle, and compliance audit-verify.
governor --helpOpenTelemetry
Every decision is a span with governor.decision and governor.rule_id attributes, inside the trace you already collect.
OTEL_EXPORTER_OTLP_ENDPOINTSIEM and GRC
Decisions stream to Splunk, Datadog, Sentinel, Chronicle and S3 in OCSF. Evidence bundles post to ServiceNow IRM, Archer, Vanta and OneTrust.
governor_integrationThe frameworks you already build with.
Each integration is the same execute call behind the framework's own hook, and each says where it intercepts. Where a framework exposes no before-execution hook, the MCP proxy covers it and the agent still changes nothing.
@tool; agent role becomes agent_id.before execute · governor-sdk[crewai] Vercel AI SDKMiddleware on tool() execute; edge runtimes.before execute · TypeScript SDK Google ADKPlugin on before_tool_callback; Vertex Agent Engine.before execute · governor-sdk[gcp] Microsoft Agent FrameworkFunction middleware; Semantic Kernel filter.before execute · governor-sdk[semantic-kernel] LlamaIndexWrapper for FunctionTool and workflow steps.before execute · governor-sdk[llamaindex] AWS BedrockAction-group executor proxy for Bedrock Agents Classic; an AgentCore Gateway interceptor is planned.before dispatch · use-cases/bedrock Anything with MCPClaude, Cursor, ChatGPT connectors, Copilot Studio — through the proxy.before dispatch · governor-mcp-proxy Framework tiles open the integrations overview; Bedrock and MCP open their use-case pages. Per-framework install, supported versions, interception timing, verdicts and failure behavior are in the SDK documentation that comes with access. LangGraph's own human-in-the-loop middleware stays useful; GovernorAI makes the approval a reviewed policy with a record, shared across every agent, instead of a graph node in one of them.
Everything has a permanent address, and a last-verified release.
Public today: the Python SDK on PyPI and every page on this site. Everything else here comes with design-partner access. Request a briefing and an engineer sets up your workspace URL, API key and org ID, the governor CLI, the release images and the docs.
With design-partner access API referenceOpenAPI 3.1, Swagger UI, versionedIn your workspace GraphQL playgroundSchema, introspection, saved queriesIn the console, signed in CLI referenceEvery command, flag and exit codeShips with your release Terraform providerProvider docs and resource schemasShips with your release Python SDK on PyPIgovernor-sdk 0.4, public. The TypeScript client ships with accesspip install governor-sdk ReleasesSigned binaries and images, with provenanceShips with your release Source and examplesSDKs, adapters, examples, the providerPrivate repository, on request ChangelogEvery release, API change and deprecation, datedIn the console, signed in StatusControl plane, sandbox and console uptimeShared with design partners For coding agentsllms.txt and a docs MCP server for Claude Code, Cursor and CopilotWith design-partner access Talk to an engineerThe people who built the adapters, on your integrationBook through a briefing Additive changes only. Deprecations are announced in the changelog twelve months before removal and surface as a response header the SDKs log.
API keys are bound to an org and a set of namespaces with read, decide or admin scope. Two keys can be live during rotation. Your IdP's JWTs work in place of a key.
Install, supported versions, interception timing, available context, supported verdicts, failure behavior and the release it was last verified against — on each integration's page, kept current by its tests.
Your first governed call
Start with one tool call. Make the boundary real.
Prove the deny. Exercise the approval. Inspect the record. Then expand — without scheduling a meeting to get there.