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.
Honesty note — what each seam cannot do

Forward-auth cannot redact. The auth_request pattern has no body mutation, so that adapter is allow, deny or approve only; when a policy asks for a payload rewrite it emits X-Governor-Shaping-Required: true so the operator knows a redaction was requested and not applied — use ext_proc or the MCP proxy where the payload must actually change. Third-party gateways also carry their own defaults: Portkey's guardrail webhook times out at roughly three seconds and fails open, which is why our endpoint never returns a 5xx to it and instead answers 200 with verdict:false; LiteLLM's post-call hook on streaming responses is audit-only and cannot block. Our side always fails closed. The gateway-side hook must still be configured to treat a webhook error or timeout as a deny, and we cannot force that from our side — it is a per-deployment check.

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.

Honesty note — what "~5 minutes" means

The integration guide's time to first governed call is about five minutes: install, set GOVERNOR_URL and GOVERNOR_API_KEY, wrap one function, watch the audit row appear. That is not time to production coverage. Coverage is a function of how many call sites you wrap — a path the SDK does not wrap stays ungoverned, which is precisely the argument for inserting at a gateway where one seam covers every call that crosses it. Two further limits worth knowing before you plan: the async client does not read environment variables, so base_url and api_key must be passed explicitly; and approval waiting is application-shaped — the SDK exposes the approval URL rather than a built-in waiter.

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
Honesty note — outcomes are seam-dependent

Allow, deny and approve hold everywhere an adapter or the SDK is in the path. Everything richer depends on what the seam can express. Redaction requires a transport that permits body mutation, which is why ext_proc and the MCP proxy can rewrite a payload and forward-auth cannot. End-to-end shaped arguments returned to an inbound self-execute caller are materialized for the AWS Bedrock seam today; generalizing that gate is tracked work, and until it lands the other adapters enforce allow, deny and pause end-to-end while redaction bodies follow. Read a claim about enforcement as a claim about a specific seam, and check which one.

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.
Honesty note — a gate that cannot lie about itself

The action exposes outcome and exit-code as outputs, and a fail-on-block input controls whether the step itself fails. What is deliberately not available is a configuration in which a degraded or blocked result is reported as success. You can choose not to fail the build; you cannot make the gate say it passed.

Continue