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.
[](#)
[](LICENSE)
[](https://nodejs.org/)
[](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).This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues