Developers

Three ways in. Two of them change nothing in the agent.

GovernorAI asks one question at one endpoint — may this call proceed, with these arguments, right now — and returns allow, deny or pause. What varies is where you ask it from. Insert at a gateway you already operate, wrap a function in a few lines, or declare the policy in Terraform.

  • POST /api/v1/gateway/execute
  • allow · deny · pause
  • Fail-closed by contract
Lines changed inside the agent 0  
loading…

PATH ONE · NO SDK IN THE AGENT

If a gateway already sees the call, that is where the decision goes.

Most agent traffic already crosses something you run — an Envoy-family gateway, an NGINX or Traefik hop, an MCP client, or an LLM gateway such as Portkey or LiteLLM. Each insertion adapter is a thin protocol translator: it parses the tool call off the wire, builds the canonical execute request, and asks the gateway for the verdict. None of them re-implement policy, data controls, approvals, kill switches or evidence — there is one decision core, and the adapters are in front of it.

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_proc governor-extproc
:9002 gRPC
Envoy Gateway, Istio, Gloo, Envoy AI Gateway, agentgateway allow → CONTINUE · redact → body mutation · pause → 202 · deny → 403
Transparent MCP proxy governor-mcp-proxy
:9003 HTTP
No gateway at all — point the MCP client's server URL at the proxy; also chainable inside an existing MCP gateway allow → forward · redact → rewrite params.arguments · pause / deny → JSON-RPC error. initialize, tools/list, ping and notifications pass through untouched
HTTP forward-auth governor-checkgw
:9004 HTTP
NGINX auth_request, Traefik ForwardAuth, an AWS Lambda authorizer, Apigee, Azure APIM allow → 200 · pause / deny → 403. The JSON verdict mode returns decision, reason, shaped_args and args_constrained
LLM-content coexistence governor-checkgw
:9004 HTTP
Portkey BYO-guardrail webhook; LiteLLM CustomGuardrail Governs the model call itself. allow → verdict:true · redact → transformed messages · deny, pause or error → 200 with verdict:false

Every binary reads the same four variables: GOVERNOR_GATEWAY_URL (default http://localhost:8080), GOVERNOR_API_KEY, GOVERNOR_ORG_ID and GOVERNOR_DECISION_TIMEOUT_MS (default 5000).

nginx.conf · forward-auth
# the tool route asks GovernorAI first; 403 stops it here
location = /_govern {
  internal;
  proxy_pass http://127.0.0.1:9004/check;
  proxy_set_header X-Governor-Tool     $http_x_governor_tool;
  proxy_set_header X-Governor-Agent-Id $http_x_governor_agent_id;
}

location /tool/ {
  auth_request /_govern;
  proxy_pass http://upstream;
}
shell · transparent MCP proxy
# no gateway required — the proxy IS the seam
GOVERNOR_MCP_UPSTREAM_URL=https://mcp.internal/servers/payments \
GOVERNOR_MCP_ID=mcp-payments \
GOVERNOR_API_KEY=gsk_… \
  ./governor-mcp-proxy          # listens on :9003

# then point the MCP client's server URL at the proxy.
# tools/call is governed. Everything else passes through.

PATH TWO · A FEW LINES

Wrap the call. Keep the credentials, the tool code and the control flow.

Where there is no seam to insert at — a plain Python 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 POST /api/v1/gateway/execute and 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.

python · one-shot wrap
# pip install governor-sdk
from governor import govern_call, GovernorAIDenied

def create_case(customer_id: str) -> dict:
    # your implementation, your credentials
    return {"case_id": "CASE-001"}

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.reason, e.rule_id)
python · decorator, and the low-level client
from governor import governed_fn

# every call to this function is now governed; the
# decorator records positional and keyword arguments
# as the call's args, so policies can match on values
@governed_fn("crm.create_case", agent_id="support-agent")
def create_case(customer_id: str) -> dict:
    return {"case_id": "CASE-001"}

from governor import GovernorAIClient

client = GovernorAIClient(
    base_url="https://<your-governorai-host>",
    api_key="gov_xxx",
)

# for anything that is not a Python function —
# a CLI invocation, a shell exec, a database write
client.execute(
    tool="payments.refund",
    args={"amount": 8200},
    agent_id="billing-agent",
    session_id="conversation-abc",
    namespace="production",
)
The property that matters On deny, the wrapped function is never invoked.

This is the difference between governance and logging. A denied call raises GovernorAIDenied — carrying the policy reason and the rule that produced it — before fn runs, so the write to the system of record does not happen and then get reported. A paused call raises GovernorAIApprovalRequired with the approval URL; the function is likewise not called. Allow returns whatever your function returns, unchanged.

PATH THREE · DECLARATIVE

Policy as a reviewed artifact, not a console setting.

The Terraform provider makes the governance objects declarable alongside the infrastructure they govern. A policy change becomes a pull request with a diff, a reviewer and a plan — the same review your team already runs for everything else that can break production.

terraform · governor provider
provider "governor" {
  server_url = var.governor_url
  api_key    = var.governor_api_key
  org_id     = "default"          # optional — defaults to "default"
}

resource "governor_namespace" "production" {
  name        = "production"
  description = "Agents that can act on systems of record"
}

resource "governor_policy" "refund_approval" {
  name        = "refund-approval"
  description = "High-value refunds require a human decision"
  enabled     = true
}

resource "governor_policy_assignment" "refunds_in_prod" {
  policy_id    = governor_policy.refund_approval.id
  namespace_id = governor_namespace.production.id
}
governor_policy

The rule itself, with enabled and a computed version that increments on each update.

governor_namespace

The logical grouping agents belong to — production, staging, and so on.

governor_policy_assignment

Binds a policy to a namespace or to one agent; an agent binding takes precedence over the namespace.

governor_agent

Agent registration: name, namespace, provider, tags, capabilities; status is computed.

governor_approval_workflow

The human gate a policy can require before an action proceeds.

governor_kill_switch

Agent, reason, and who activated it — the stop control, with the attribution attached.

governor_automation_config

Discovery, lifecycle, policy and onboarding modes: manual, auto or hybrid.

governor_integration

The connections GovernorAI reads from and writes evidence to.

Why declare it instead of clicking it 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 the change — is answered by your version control rather than by a screenshot. The separation of duties is the one your pipeline already enforces; GovernorAI does not ask for a second one.

WHAT YOU GET BACK

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

The response carries decision — allow, deny or pause — with reason and rule_id populated on a deny, and approval_url on a pause. The adapters additionally surface shaped_args and args_constrained where the seam can carry a rewritten payload.

POST /api/v1/gateway/execute
LINEAGE

Session, agent, namespace

Every audit row carries the session_id. Pass a stable one per conversation and the full tool sequence for a run can be replayed in order — what was attempted, what was decided, and what the agent did next.

tamper-evident audit row per call
GRAPHQL

Query the decision record

A GraphQL endpoint with a published schema and a playground. Queries include agents, policies, events (filterable by agent, decision and time range), approvals, alerts and automationLogs.

/api/v1/graphql
WEBHOOKS

Pushed, signed, replayable

Events include policy.decision, approval.requested, approval.resolved, killswitch.activated and agent.registered. Deliveries carry X-Governor-Event, X-Governor-Delivery, an idempotency key and an HMAC-SHA256 X-Governor-Signature-256, with dead-letter handling and replay.

signed · idempotent · replayable
OPENAPI

A spec, and SDKs from it

The REST surface is published as OpenAPI with a Swagger UI, and client SDKs are generated from it for Python, TypeScript, Go, Java, C# and Ruby.

/api/v1/openapi.yaml
CLI

The same operations, scriptable

governor-cli covers agents, policies, guardrails, kill switches, approvals and governance lifecycle — including compliance audit-verify, which verifies the integrity of the audit log rather than asking you to trust it.

governor-cli

HOW IT FAILS

Closed. Every time, for every reason.

A governance component that fails open is a governance component that an attacker only has to make unreachable. The shared decision client the adapters are built on resolves every failure mode to the same verdict.

Transport error

The gateway cannot be reached at all — DNS, TLS, connection refused.

→ deny
Timeout

No answer inside GOVERNOR_DECISION_TIMEOUT_MS, 5000 by default.

→ deny
Non-2xx response

The gateway answered, but not with a decision.

→ deny
Unparseable body

A 200 whose body is not a verdict the adapter can read.

→ deny
Stated precisely A synthesized deny is marked as one, and still blocks.

When the client denies because the gateway was unreachable rather than because a policy said no, the response is flagged fail_closed so logs and operator messaging can tell the two apart. The enforcement action is identical — the call is blocked either way. On the Envoy filter this is why failure_mode_allow must be set to false: it is the one line that decides whether an unreachable processor stops the call or waves it through.

CI · THE ASSURANCE GATE

Fail closed in the pipeline too.

The assurance gate runs one lifecycle step per invocation, so your own deployment sits between deploy-begin and promote rather than inside a fixed step list. Its exit codes distinguish a verdict about the candidate from a problem with the gate itself — a distinction that matters when someone asks whether a red build meant the agent was blocked or the control plane was down.

github actions · one step per lifecycle stage
- uses: ./.github/actions/assurance-gate
  with:
    command: gate
    server-url: ${{ vars.GOVERNOR_URL }}
    api-key: ${{ secrets.GOVERNOR_API_KEY }}
    image-digest: ${{ steps.build.outputs.digest }}

# the flow, for today's targetless state:
#   register → assess → gate → deploy-begin
#     → [your deployment] → promote
Assurance gate exit codes and what each one means.
ExitMeaning
0Pass, or risk accepted.
1Blocked — a verdict about the candidate.
2Usage or malformed configuration. Not a verdict about the agent.
3Control plane unreachable after the retry budget. Never 0, even with --allow-degraded.
4Authentication or authorization failure. No flag makes this tolerable.

Continue