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.

Run the quickstart Find your seam Docs, SDKs and access The quickstart ends in a verified deny and the record it left. Nothing is installed inside the agent.
Keep your stackStart with a function, an MCP server or a gateway. Native platform controls stay in the design. Keep controlBusiness rules are owned outside the model's discretion, and outside any single agent's release. Keep the proofPermission, human authority, dispatch and the target's result are four facts, recorded separately.
payments.refund· the same tool, four different outcomeslocal · no API call

Same tool. Different decision.

The agent can call payments.refund. Should this refund proceed?

$
demo policy refund-approval v7 · ≤ 5,000 allow · ≤ 15,000 pause for finance-managers · above 15,000 deny
·Evaluate to get a verdict.

Nothing has been decided. The target has not been called.

01 policy—
02 authority—
03 dispatch—
04 result—
Synthetic workflow. Approval never executes a tool, and a held call is not released: dispatch applies to an allow. A lost target response is an unknown outcome, not a failure — reconcile, don't retry.
Why add this, even inside one cloud

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.

business rules

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.

team autonomy

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.

operational evidence

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.

Manage the controls as code. Enforce them in the action path.

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.

Policy as code
Inspect the boundary before choosing a path: what reaches the check, and what to validate
PathWhat reaches the checkVerdicts it can applyWhat to validate before production
Function wrapperExplicitly wrapped invocations and the arguments you passallow · deny · pause · shaped argsDeny and pause prevent the function body. An unwrapped function or an alternate credential path is outside this wrapper.
MCP proxyRouted tools/call requests: tool name, arguments, per-call identity headersallow · deny · pause · shaped argsIdentity header handling, JSON-RPC error mapping, upstream delivery. Handshake and discovery are passed through and are not tool execution.
Envoy ext_procHeaders and the buffered request bodyallow · deny · pause · body mutationBuffering 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 headersallow · deny · pauseA 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 hookallow · deny · pause · transformed messagesA model-call check does not, by itself, intercept the external tool's eventual execution. Pair it with a tool-boundary seam.
SidecarWhatever the adapter or SDK beside it sends; evaluated against its mounted policy files and the signed Rego and kill-switch bundleallow · deny · pause · shaped argsBundle signature and version, TTL behavior when the control plane is unreachable, evidence replay on reconnect.
Cloud / SaaS connectorDepends on the specific connection and action pathVaries by platformDistinguish 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.

Your code · wrap the function

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.

Prepare the environment

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.*
Attach a test policy

A sandbox namespace and a policy that matches the decorated tool name and its exported argument shape.

governor policies create -f refund-approval.yaml
Test deny before allow

Assert 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.py

On 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.
first_run.py · a harmless stub, so the first success is a verified deny
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}")
python · one-shot wrap, and the low-level client
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 row
tests/test_boundary.py · "the target was never called" is an assertion
import 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()
typescript · govern a function
// 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);
}
This seam can: allow, deny, pause, hand you shaped args. It cannot: see a call that bypasses the wrapper.
Your tools · route through the MCP proxy

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.

Get the proxy

Container image or static binary for linux amd64/arm64. One proxy fronts one upstream MCP server.

--build-arg BINARY=governor-mcp-proxy
Run it

Configured entirely by environment. GOVERNOR_MCP_UPSTREAM_URL is the only required value.

:9003
Repoint the client

Change the MCP client's server URL to the proxy. Nothing else in the client changes.

http://localhost:9003
Write one deny rule

One tool, denied unconditionally — narrow enough to trigger on purpose.

governor policies create -f deny-refund.yaml
Call it, read the record

JSON-RPC error -32002 comes back; the event row carries rule_id and action_id.

GET /api/v1/events?decision=deny
Prove it fails closed

Stop the policy engine, call again. Denied, flagged fail_closed. No knob turns that off.

governor compliance audit-verify
shell · the proxy is the seam
# 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.
deny-refund.yaml · a file, not a console setting
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: deny
the denied tools/call · HTTP 200, verdict in the JSON-RPC error
HTTP/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.
This seam can: allow, deny, pause, rewrite arguments. It cannot: govern a tool the client reaches without the proxy.

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/call with no tool name, or one inside a JSON-RPC batch: all resolve to -32002 and 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_proc is the shorter route in production.
Full quickstart, step by step
Your infrastructure · insert at a gateway you run

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.

Gateway insertion adapters, the binary and port each runs as, what it inserts into, and how each verdict is expressed on that protocol.
AdapterBinary · portInserts intoVerdict → action on the wire
Envoy ext_procgovernor-extproc :9002 gRPCEnvoy Gateway, Istio, Gloo, Envoy AI Gateway, agentgatewayallow → CONTINUE · redact → body mutation · pause → 202 · deny → 403
HTTP forward-authgovernor-checkgw :9004 HTTPNGINX auth_request, Traefik ForwardAuth, AWS Lambda authorizer, Apigee, Azure APIMallow → 200 · pause / deny → 403. Headers only: no body inspection, no rewrite on this seam.
LLM-content coexistencegovernor-checkgw :9004 HTTPPortkey BYO-guardrail webhook; LiteLLM CustomGuardrailGoverns 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).

envoy.yaml · body-aware; the adapter extracts arguments from the buffered body
- 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.
Can: allow, deny, pause, rewrite the body (shaped args). Validate: buffer limits, extraction for your tool schema, the filter's own failure setting.
litellm config.yaml · governs the model call
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
Can: allow, deny, pause, transform messages on the model call. Cannot: stop the external tool the model's answer later triggers — pair with a tool-boundary seam.
portkey · BYO guardrail webhook
# 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
Can: allow, deny, pause on the model request and response; Portkey's own MCP guardrails cover tool I/O schema checks. GovernorAI adds: the business rule, the approval and the evidence record across every seam.

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

Your hosts · run the sidecar

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.
Kubernetes & sidecar
Sidecar install, with an engineer Sidecar installs are set up with an engineer during a briefing while we finish verification of this install path. Request a briefing

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.

POST /api/v1/gateway/execute
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" });
Response when the policy says
{ "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.
The verdict is in the body, not the status code. Every response carries action_id, the join to the audit row.
Manage the controls as code

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.

refund-approval.yaml · the demo policy from the top of the page
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 workflow
refund_approval.rego · for the OPA you already run
package 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

Author

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.yaml
Test

Replay 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-approval
Review

A pull request, a reviewer, a plan. The version increments on merge; decision records do not yet cite it.

terraform plan
Roll out

Observe mode first: decisions are logged, nothing is blocked, existing protections stay active. Flip to enforce per namespace or per agent.

governance_mode: shadow
Watch, and roll back

The 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 6

Let 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, enabled and its rules, with a computed version.
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.
terraform · the policy, its approval and its assignment as resources
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.

Routine allowFunction runs once; action_id appears on the event row.
Policy denyFunction never runs; rule_id is the one you expect.
Approval pendingFunction never runs; approval_url is present; expiry is set.
Approval does not releaseApprove a held request: the decision is recorded and nothing is dispatched.
Decision timeoutGateway stopped: deny with fail_closed: true; nothing dispatched.
Duplicate dispatchRetry after a lost response does not issue a second refund.

The operational questions worth answering before rollout

Identity and tenant context
Resolve tenant, principal and agent identity at a trusted boundary — the gateway's validated JWT, the MCP session, the SDK's own credential. Strip or overwrite caller-supplied identity headers; an agent label from the client is context, not authentication. Test cross-tenant requests and conflicting identities.
Timeouts, outages and local policy
The Envoy ext_proc, MCP proxy and forward-auth adapters have an explicit decision deadline and fail closed in enforce mode. The sidecar keeps deciding on its last verified bundle until the bundle's TTL, then denies. When the approval service is down a pause stays pending; when the evidence path is down the sidecar buffers and replays, and the gateway path refuses rather than drops. A configured timeout is a bound, not a latency result.
Retries and uncertain target outcomes
A lost response after dispatch means the outcome is unknown — not that the refund failed. Use the target's idempotency key (GovernorAI passes 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
The hook runs before the side effect and propagates cancellation and errors. When arguments are shaped, the exact outbound payload is 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
Observe a candidate policy while existing protections stay active; review the verdict diff; flip to enforce per namespace or agent; roll back by version pin. Observe-mode decisions are recorded as observed, never as blocked.
Logs, privacy and evidence availability
Decision events store a hash of the arguments; an approval request keeps the arguments it asks a person to approve. The fields sent for evaluation, redaction before logging and retention are documented per seam. 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.
Transport error

DNS, TLS, connection refused.

→ deny
Timeout

No answer inside the decision timeout, 5 seconds by default.

→ deny
Non-2xx response

Even one whose body says allow.

→ deny
Unparseable body

Not a verdict the client can read, or over 1 MiB.

→ deny
illustrative, not a published action · .github/workflows/deploy.yml · one step per lifecycle stage; your deployment sits between deploy-begin and promote
- 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] → promote
exit codes · a verdict about the candidate is not a problem with the gate
0  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 tolerable

Measure 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.

TransportThe hop to the adapter, proxy, sidecar or control plane.
Decode and mapParse the tool call off the wire, build the canonical request.
Policy and checksEngine evaluation, plus any detector or classifier the policy invokes.
EvidenceRecorded write, or buffered batch on the sidecar.
ApplyExpress the verdict on the protocol: 403, JSON-RPC error, body mutation.
In processPolicy evaluation, beside the agent

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.

End-to-end overhead is reported per seam as p50 / p95 / p99 from the load harness, on the production instance type.
Seamp50p95p99Conditions
Sidecarin measurementEKS, c7g, 50-rule policy, evidence batched
MCP proxyin measurementsame host as control plane
Envoy ext_procin measurementbuffered body ≤ 64 KiB
SDK → control planein measurementsame 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/execute

Lineage

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/graphql

Webhooks

policy.decision, killswitch.activated, agent.registered. HMAC-SHA256 signed, idempotency key, dead-letter and replay.

X-Governor-Signature-256

OpenAPI

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.yaml

CLI

One binary, governor: agents, policies, guardrails, kill switches, approvals, lifecycle, and compliance audit-verify.

governor --help

OpenTelemetry

Every decision is a span with governor.decision and governor.rule_id attributes, inside the trace you already collect.

OTEL_EXPORTER_OTLP_ENDPOINT

SIEM and GRC

Decisions stream to Splunk, Datadog, Sentinel, Chronicle and S3 in OCSF. Evidence bundles post to ServiceNow IRM, Archer, Vanta and OneTrust.

governor_integration

The 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.

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.

v1 is stable.

Additive changes only. Deprecations are announced in the changelog twelve months before removal and surface as a response header the SDKs log.

Keys are scoped, and they rotate.

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.

Every integration carries a support contract.

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.

Continue