Skip to main content
Glama
TH07008

agentic-governance-gateway

by TH07008
README.md
# Agentic Governance Gateway

> An open-source governance layer that sits between coding agents and your systems.
> Policies, provenance, validation and human-in-the-loop — with optional transparent
> proxy enforcement, agent identity, privilege rings, and Prometheus observability.

[![CI](https://img.shields.io/badge/CI-passing-brightgreen)](#)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Node](https://img.shields.io/badge/node-%E2%89%A520-339933)](https://nodejs.org/)
[![OPA](https://img.shields.io/badge/OPA-optional-7c3db8)](https://www.openpolicyagent.org/)

## Why does this exist?

Agentic coding tools (Claude Code, Cursor, Copilot, …) generate code faster
than humans can review it. The 2026 industry reports agree on one thing:
**governance is the bottleneck**, not code generation.

- 92 % of enterprises report governance challenges with AI-generated code.
- 82 % had at least one production incident caused by AI-generated code in the
  last six months.
- 60 % deploy untested AI-generated code.
- 43 % cannot reliably tell whether code was written by a human or an agent.

`agentic-governance-gateway` is a small, framework-agnostic control plane
that sits in front of any tool an agent wants to call. Every action flows
through one canonical pipeline:

```
AgentAction
  → Budget check          (cost estimation, daily/monthly caps)
  → Policy evaluation      (OPA/Rego when available, JS fallback otherwise)
  → Validation             (pluggable checkers: semgrep, npm test, checkov, …)
  → Human-in-the-loop      (review for sensitive actions, hash-verified execution)
  → Execution              (injected tool executor)
  → Audit + provenance     (W3C PROV-O records, immutable trail)
```

## Features

- **Policy-as-code** with OPA/Rego, plus a TypeScript fallback so the gateway
  works with zero external dependencies.
- **Traceability** with W3C PROV-O provenance and an in-memory or SQL store.
- **Validation orchestration** with pluggable checkers (StaticChecker and
  ScriptChecker ship out of the box).
- **Human-in-the-loop** with hash-verified execution: the action the human
  approved is the action that runs, or it is denied.
- **Budget & cost control** with per-agent daily/monthly caps.
- **MCP server** that drops into Claude Code, Cursor and any MCP-compatible
  client — no API key required.
- **REST API** for non-MCP clients with `X-API-Key` authentication, CORS,
  security headers, and request body size limits.
- **CLI** for ad-hoc policy checks from the terminal.
- **Rate limiting** with a configurable per-agent sliding window (default:
  60 actions/minute, set via `RATE_LIMIT_PER_MINUTE`).
- **REST API authentication** via `X-API-Key` header. The API is fail-closed:
  without `GATEWAY_API_KEY`, all non-healthz routes return 401.
- **PostgreSQL persistence** for audit trail and budget state via `DATABASE_URL`.
  Falls back to in-memory storage when unset.
- **CORS support** configurable via `CORS_ORIGIN` (default: `*`).
- **Data retention** with a `POST /admin/purge` endpoint to delete audit
  records older than a given date.
- **Security headers** (`X-Content-Type-Options`, `X-Frame-Options`,
  `Referrer-Policy`) on all REST responses.
- **Log redaction** — `prompt` and `params` fields are automatically redacted
  in structured log output.
- **Proxy mode (enforcement)** — transparent MCP stdio proxy that intercepts every
  `tools/call`, runs it through the governance pipeline, and only forwards to the
  backend on `allow`. The agent cannot bypass the gateway. See
  [Proxy Mode](#proxy-mode-enforcement) below.
- **Agent discovery** — the proxy tracks every connected agent (ID, version, session,
  tool-call count, deny count). Unknown agents are flagged via `AGENT_ALLOWLIST`.
- **Agent identity** — optional token/certificate authentication with SHA-256
  credential hashing and per-agent privilege rings (0 = full, 1 = sandboxed,
  2 = read-only).
- **Privilege rings (containment)** — policy-level containment: Ring 2 agents cannot
  use mutating tools, Ring 1 agents are path-confined to `WORKSPACE_ROOT`.
  Per-agent tool allowlists via `AGENT_TOOL_ALLOWLIST`.
- **Observability** — Prometheus metrics at `/metrics`, an inline HTML dashboard at
  `/dashboard`, and a live agent list at `/agents`.

## Quick start

```bash
npm install
npm run build
npm test            # unit + integration + rego tests
npm run test:e2e    # MCP handler end-to-end tests
```

Run the CLI:

```bash
node dist/cli/index.js policies              # list built-in rules
node dist/cli/index.js status               # show active evaluator
node dist/cli/index.js evaluate \
  --tool write_file \
  --params '{"path":"prod/secrets.yml"}' \
  --prompt "rotate the password"
# → decision: require_review / deny / allow

Start as a transparent proxy:

```bash
node dist/cli/index.js proxy \
  --backend-command npx \
  -- mcp-server-filesystem /workspace
# → agent connects, every tool/call is intercepted
```
```

## Configuration

All configuration is via environment variables (Twelve-Factor). See
`.env.example` for a complete list.

| Variable | Default | Description |
|----------|---------|-------------|
| `LOG_LEVEL` | `info` | Log level: trace, debug, info, warn, error |
| `OPA_POLICY_DIR` | `./policies` | Directory with Rego policy files |
| `OPA_WASM_PATH` | — | Path to pre-built OPA WASM bundle. When set, OPA is the primary evaluator |
| `POLICY_FALLBACK_JS` | `true` | Fall back to TypeScript evaluator when OPA unavailable |
| `DATABASE_URL` | `memory` | PostgreSQL connection string. When unset or `memory`, uses in-memory storage |
| `DEFAULT_DAILY_BUDGET_USD` | `50` | Default per-agent daily budget cap |
| `DEFAULT_MONTHLY_BUDGET_USD` | `1000` | Default per-agent monthly budget cap |
| `HITL_TIMEOUT_SECONDS` | `300` | Review request timeout before auto-deny |
| `GATEWAY_API_KEY` | — | REST API key. **Required** for REST API to function. Without it, all non-healthz routes return 401 |
| `CORS_ORIGIN` | `*` | Allowed CORS origin for REST API |
| `RATE_LIMIT_PER_MINUTE` | `60` | Max actions per agent per minute |
| `MCP_SERVER_NAME` | `agentic-governance-gateway` | MCP server name reported to clients |
| `MCP_SERVER_VERSION` | `0.1.0` | MCP server version |
| `BACKEND_COMMAND` | — | Backend MCP server command (e.g. `npx`). When set, the gateway runs as a transparent proxy |
| `BACKEND_ARGS` | — | JSON array of arguments for the backend command (e.g. `["mcp-server-filesystem","/workspace"]`) |
| `BACKEND_ENV` | — | JSON object of environment variables for the backend process |
| `BACKEND_CWD` | — | Working directory for the backend process (optional) |
| `AGENT_AUTH_ENABLED` | `false` | Enable agent identity verification on MCP `initialize` |
| `AGENT_ALLOWLIST` | — | Comma-separated list of allowed agent IDs (e.g. `claude-code,cursor`) |
| `SHADOW_AGENT_POLICY` | `warn` | Policy for unknown agents: `warn` (log only) or `deny` (block) |
| `DEFAULT_PRIVILEGE_RING` | `1` | Default privilege ring for unauthenticated agents (0=full, 1=sandboxed, 2=read-only) |
| `WORKSPACE_ROOT` | `.` | Root directory for path confinement (Ring 1 agents cannot write outside this) |
| `AGENT_TOOL_ALLOWLIST` | — | Per-agent tool allowlist (format: `agentId:tool1,tool2;agentId2:tool3`) |
| `METRICS_ENABLED` | `true` | Collect Prometheus metrics |
| `METRICS_PORT` | `9090` | Port for the `/metrics` endpoint |
| `DASHBOARD_ENABLED` | `true` | Enable the HTML dashboard at `/dashboard` |

### Production checklist

- [ ] Set `GATEWAY_API_KEY` (generate with `openssl rand -hex 32`)
- [ ] Set `DATABASE_URL` with `?sslmode=require` for TLS
- [ ] Set `CORS_ORIGIN` to your actual origin (not `*`)
- [ ] Place behind a TLS-terminating reverse proxy (nginx, Caddy, Traefik)
- [ ] Configure log aggregation (Pino outputs JSON to stdout)
- [ ] Set up a retention cron for `/admin/purge`

## Connecting an MCP client

`~/.cursor/mcp.json` (Cursor) or the equivalent for Claude Code:

```json
{
  "mcpServers": {
    "agentic-governance-gateway": {
      "command": "node",
      "args": ["/absolute/path/to/dist/mcp/main.js"]
    }
  }
}
```

The gateway then exposes three tools to the agent:

| Tool                     | Purpose                                            |
| ------------------------ | -------------------------------------------------- |
| `governed_tool_call`     | Submit a tool call through the governance pipeline |
| `governance_status`      | Report evaluator + budget snapshot                 |
| `governance_audit_lookup`| Fetch an audit record by action id                  |

Agents are expected to call `governed_tool_call` instead of touching files,
git or shell commands directly.

## Proxy Mode (Enforcement)

The cooperative mode above relies on the agent choosing to route its calls
through the gateway. **Proxy mode** eliminates that assumption: the gateway
intercepts every tool call transparently — the agent cannot bypass it.

### How it works

```
┌──────────┐    MCP (stdio)     ┌──────────────────────────────┐
│  Agent   │ ◄─────────────────►│      GATEWAY PROXY           │
│ (Claude) │   tools/list → fwd │                              │
│          │   tools/call →     │  Governance Pipeline         │
│          │     intercept      │  Rate-Limit → Budget →       │
│          │   initialize → fwd │  Policy → Validation → HITL  │
│          │                    │                              │
│          │                    │   allow → forward to backend │
│          │                    │   deny  → MCP error response │
└──────────┘                    └──────────────┬───────────────┘
                                               │ MCP (stdio)
                                               ▼
                                    ┌──────────────────┐
                                    │  Backend MCP     │
                                    │  Server (real)   │
                                    │  filesystem,     │
                                    │  git, bash, ...  │
                                    └──────────────────┘
```

The proxy implements **both sides** of the MCP protocol:

1. **Server side** (toward the agent): uses `StdioServerTransport`. The agent
   thinks it is talking to the real backend.
2. **Client side** (toward the backend): spawns the real MCP server as a child
   process via `StdioClientTransport`.
3. **Interception**: `tools/list`, `resources/*`, `prompts/*`, and `initialize`
   are forwarded 1:1. Only `tools/call` is intercepted — the tool name and
   arguments are mapped to an `AgentAction`, run through the governance
   pipeline, and forwarded only on `allow`. On `deny`, the agent receives a
   MCP-conformant error response with the reason.

### Deployment

Start the proxy from the CLI:

```bash
agentic-gateway proxy \
  --backend-command npx \
  -- mcp-server-filesystem /workspace
```

Or via environment variables:

```bash
BACKEND_COMMAND=npx BACKEND_ARGS='["mcp-server-filesystem","/workspace"]' \
  node dist/proxy/proxy-main.js
```

### Agent configuration

Point the agent at the proxy instead of the backend. The agent never sees
the backend server:

```jsonc
// ~/.config/claude-code/mcp.json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "agentic-governance-gateway", "proxy",
        "--backend-command", "npx",
        "--", "mcp-server-filesystem", "/workspace"
      ]
    }
  }
}
```

The agent sees the backend's tools (e.g. `read_file`, `write_file`, `bash`) via
`tools/list` — the proxy forwards this transparently. When the agent calls a
tool, the proxy intercepts and runs the governance pipeline. On `deny`, the
agent receives:

```
Tool call denied: Path outside workspace root: /etc/passwd
```

### Limitation: prompt-based policies

In proxy mode, the gateway sees the tool call but not the user's prompt.
Policies that rely on `prompt` content (SSN detection, AWS key detection) are
skipped gracefully — the proxy logs a warning and continues. Tool- and
parameter-based policies (path confinement, `rm -rf` blocking, `DROP TABLE`
detection, prod-path protection) work fully in proxy mode.

### Discovery & identity

The proxy registers every agent that connects via MCP `initialize`:

| Field | Source |
|-------|--------|
| `agentId` | `clientInfo.name` from the `initialize` request |
| `agentVersion` | `clientInfo.version` |
| `connectedAt` | Timestamp at connection |
| `toolCalls` | Incremented on each intercepted `tools/call` |
| `denyCount` | Incremented on each denied call |

When `AGENT_AUTH_ENABLED=true`, the proxy verifies a token from the
`initialize` request's `_meta.auth` field against the configured credential
store. On success, an `AgentIdentity` with a privilege ring is created and
attached to every subsequent action.

Unknown agents (not in `AGENT_ALLOWLIST`) are flagged. The response is
governed by `SHADOW_AGENT_POLICY`: `warn` (log and continue) or `deny` (reject
the connection).

### Privilege rings (containment)

| Ring | Name | Capabilities |
|------|------|-------------|
| **0** | Full | All tools, all paths (trusted CI agents) |
| **1** | Sandboxed | All tools, but paths confined to `WORKSPACE_ROOT` (default for interactive agents) |
| **2** | Read-only | Non-mutating tools only (`read_file`, `list_files`, …) |

Ring assignment comes from the credential store (when authenticated) or from
`DEFAULT_PRIVILEGE_RING` (default: 1). Per-agent tool allowlists can further
restrict which tools an agent may call:

```bash
AGENT_TOOL_ALLOWLIST=claude-code:write_file,read_file,bash;ci-agent:read_file
```

### Observability

The proxy exposes three endpoints (when `METRICS_ENABLED` and
`DASHBOARD_ENABLED` are set):

- **`GET /metrics`** — Prometheus text format with counters
  (`gateway_tool_calls_total{agent,tool,decision}`), latency histograms, budget
gauges, and active-agent gauges.
- **`GET /dashboard`** — inline HTML dashboard (no external dependencies,
  auto-refreshing) showing active agents, decision rates, budget consumption,
  and the last 20 tool calls.
- **`GET /agents`** — JSON array of active agent connections with tool-call
  and deny counts.

## REST API

The REST API is optional (MCP is the primary transport). Start it with
`npm run start:api` (requires `dist/` to be built).

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/process` | Full governance pipeline (budget → policy → validation → HITL → execute → audit) |
| `POST` | `/evaluate` | Policy evaluation only — no execution, no budget charge, no audit record |
| `GET`  | `/status/:agent` | Budget snapshot for an agent |
| `GET`  | `/audit/:id` | Audit record by action id |
| `GET`  | `/healthz` | Liveness probe (no auth required) |
| `GET`  | `/metrics` | Prometheus exposition format (text/plain, no auth) |
| `GET`  | `/dashboard` | Inline HTML dashboard (requires proxy mode with registry + metrics) |
| `GET`  | `/agents` | List active agent connections (JSON) |
| `POST` | `/admin/purge` | Delete audit records older than a given date (requires API key) |

All non-healthz routes require `X-API-Key` header. Request bodies are limited
to 1 MiB.

### Example: evaluate an action

```bash
curl -X POST http://localhost:3000/evaluate \
  -H "content-type: application/json" \
  -H "x-api-key: $GATEWAY_API_KEY" \
  -d '{
    "agentId": "claude-code",
    "sessionId": "s1",
    "tool": "write_file",
    "params": {"path": "prod/secrets.yml"},
    "prompt": "rotate the password"
  }'
# → { "decision": "require_review", "reasons": [...] }
```

### Example: process an action (full pipeline)

```bash
curl -X POST http://localhost:3000/process \
  -H "content-type: application/json" \
  -H "x-api-key: $GATEWAY_API_KEY" \
  -d '{
    "agentId": "claude-code",
    "sessionId": "s1",
    "tool": "write_file",
    "params": {"path": "src/utils.ts"},
    "prompt": "add a helper function"
  }'
# → { "decision": "allow", "audit": { "id": "...", ... } }
```

## Testing without an API key

The project is explicitly designed to be developed and tested with **no
Claude/OpenAI account**:

- The policy engine has a pure TypeScript evaluator (`JsPolicyEvaluator`)
  that mirrors the Rego policies exactly — no LLM involved.
- The MCP server is exercised end-to-end via a fake in-process server in
  `tests/e2e/mcp-handlers.test.ts`.
- Rego policies are unit-tested with `opa test policies/` (the CI job
  installs OPA automatically).
- A cross-implementation parity test (`tests/integration/policy-parity.test.ts`)
  verifies the TS and Rego evaluators agree on a shared set of inputs. It is
  skipped automatically when `opa` is not on PATH.
- For full agent loops, point the gateway at a local Ollama model
  (`model: "llama3:70b"`) — cost estimation returns 0 and no external API is
  called.

## Docker

```bash
# Set the API key first
export GATEWAY_API_KEY=$(openssl rand -hex 32)

docker compose up -d
```

The Dockerfile runs as non-root (`USER node`), includes a healthcheck, and
mounts a persistent volume for database files. See `docker-compose.yml`.

To run the proxy in Docker, set `BACKEND_COMMAND` and `BACKEND_ARGS` in the
compose service or environment.

## Project layout

```
agentic-governance-gateway/
├── src/
│   ├── core/
│   │   ├── policy-engine/   OPA + JS evaluator, rules
│   │   ├── traceability/    Audit store, PROV-O provenance
│   │   ├── validation/      Pluggable checkers + orchestrator
│   │   ├── hitl/            Human-in-the-loop gateway
│   │   ├── budget/          Cost controller
│   │   ├── gateway.ts        Canonical pipeline façade
│   │   ├── config.ts
│   │   ├── rate-limiter.ts
│   │   └── logger.ts
│   ├── proxy/               Transparent MCP proxy (enforcement mode)
│   ├── identity/            Agent identity + credential store
│   ├── observability/       Prometheus metrics + HTML dashboard
│   ├── mcp/                 MCP server + entrypoint
│   ├── api/                 REST API
│   ├── cli/                 CLI
│   └── types/                Shared TypeScript types
├── policies/                Rego policies + tests
├── tests/
│   ├── unit/                Per-module unit tests
│   ├── integration/         Gateway + parity tests
│   └── e2e/                 MCP handler tests
├── docs/                    Architecture + ADRs
├── examples/                Runnable example policies & configs
├── Dockerfile
├── docker-compose.yml
└── .github/workflows/ci.yml
```

## Architecture

See [docs/architecture.md](docs/architecture.md) for the full design and
[docs/decisions/](docs/decisions/) for the ADRs that explain the trade-offs:

- [0001 – OPA optional](docs/decisions/0001-opa-optional.md)
- [0002 – MCP-first](docs/decisions/0002-mcp-first.md)
- [0003 – HITL hash verification](docs/decisions/0003-hitl-hash-verification.md)
- [0004 – Rate limiting (in-memory)](docs/decisions/0004-rate-limiting-in-memory.md)
- [0005 – MCP proxy enforcement](docs/decisions/0005-mcp-proxy-enforcement.md)
- [0006 – Agent identity model](docs/decisions/0006-agent-identity-model.md)
- [0007 – Privilege rings](docs/decisions/0007-privilege-rings.md)

## License

MIT — see [LICENSE](LICENSE).