Documentation
Everything you need to deploy Clausgap, write policies, and integrate your AI agents.
How Clausgap works
Clausgap sits between your AI agents and every tool or API they touch. It intercepts each request, evaluates it against your policy bundle, and either allows it, denies it, rate-limits it, or holds it in a multi-party escrow queue pending human approval.
Proxy
clausgap-proxy — a single binary supporting HTTP, PostgreSQL, and MCP. Deploy one container; configure which protocols you need via a listeners array.
Control Plane
REST API for managing policies, escrow holds, events, and subscriptions. Pushes signed policy bundles to proxies.
Dashboard
Web UI for writing policies, voting on escrow holds, and inspecting the BLAKE3-chained forensic audit trail.
Agent (SDK: X-Clausgap-Context HMAC header)
(Keyless: JWT Bearer / API key / IP-CIDR — no code changes)
│
▼
clausgap-proxy ──┬──► :8443 HTTP listener ──► REST APIs / tools
├──► :5433 PG listener ──► PostgreSQL / Redshift
└──► :3000 MCP listener ──► MCP tool servers
│ Local bundle evaluation (no network round-trip on hot path)
│
├─ ALLOW → forward request, return response
├─ DENY → 403 / SQL error / JSON-RPC error
├─ ESCROW → 423 Locked + {escrow_id} (agent polls; M-of-N voting)
└─ RATE → 429 Too Many Requests
↕ Async (bundle sync, event drain, escrow state)
Control Plane API (/v1/...)Policy evaluation happens inside the proxy using a locally cached, Ed25519-signed bundle — so there is no control-plane round-trip on the hot path. Bundles are refreshed from the control plane every 30 seconds (configurable). Agent identity is resolved from the X-Clausgap-Context HMAC header (SDK mode) or, in keyless mode, from a JWT Bearer token, a registered API key, or source IP/CIDR — no agent code changes required.
Quickstart
Create a workspace, then four steps take you to a policy enforcing your agent's traffic. The Get started page in the dashboard tracks the same steps and completes each one by itself when it happens — a proxy connects, a request arrives, a policy decides one.
Create your workspace
Sign up at app.clausgap.dev/signup and open the link we email you. Creating the workspace shows your API key once and takes you to Get started.
The API key is only shown at creation time. Store it in a secret manager before leaving the page. You can issue more under API Keys.
The key looks like clk_free.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. The proxy uses it as control_plane.api_key in proxy.yaml; for the API, pass it as Authorization: Bearer <key>.
A new workspace starts in Discovery Mode for 7 days. While it is on, an agent that has no policy yet is let through and what it does is recorded, so nothing breaks on day one and your first policy can be drafted from real traffic. It never overrides a policy that exists, and a request that does not identify its agent is still denied. When it ends, an agent with no policy is denied. Get started shows the end date, and lets you extend it or turn it off; admins are emailed 48 hours before it ends if nothing is enforcing yet.
Connect a proxy
The proxy runs in your network, in front of the APIs and tools your agent calls. The fastest path is the Deploy Proxy wizard — it generates routes.yaml, proxy.yaml, Docker Compose and Helm values pre-filled with your tenant ID and a fresh HMAC secret. Or write them yourself:
routes.yaml maps tool names to upstream URLs. The tool name is what your policy rules reference.
routes:
# HTTP upstream — proxied as-is
- tool: "web_search"
upstream: "http://search-service:8080"
methods: ["GET"]
- tool: "file_write"
upstream: "http://fs-service:9000"
methods: ["POST", "PUT", "DELETE"]
# MCP / JSON-RPC server — tools auto-discovered on startup
- tool: "mcp_tools"
upstream: "http://mcp-server:5000/"
methods: ["POST"]
protocol: "jsonrpc"
discover: trueproxy.yaml configures the proxy. Values prefixed with $ are expanded from the environment.
tenant_id: "$CLAUSGAP_TENANT_ID"
log:
level: "info"
control_plane:
url: "https://api.clausgap.dev"
api_key: "$CLAUSGAP_API_KEY"
poll_interval: "30s" # How often to pull policy bundle
listeners:
# HTTP listener — governs REST API / tool calls
- protocol: http
addr: ":8443"
# advertise_addr: "agents.example.com:443" # Where agents connect, if not addr (see below)
http:
routes_file: "./routes.yaml"
hmac_secret: "$HMAC_SECRET" # Shared with your agent SDK
# Uncomment to add PostgreSQL governance:
# - protocol: pg
# addr: ":5433"
# pg:
# upstream_host: "postgres"
# upstream_port: 5432
# upstream_dbname: "mydb"# Run the binary
HMAC_SECRET=my-secret CLAUSGAP_API_KEY=clk_free.xxx \
clausgap-proxy -config proxy.yaml
# Or with Docker
docker run -d --name clausgap-proxy \
-p 8443:8443 \
-e HMAC_SECRET=my-secret \
-e CLAUSGAP_API_KEY=clk_free.xxx \
-e CLAUSGAP_TENANT_ID=my-org \
-v $(pwd)/routes.yaml:/etc/clausgap/routes.yaml \
-v $(pwd)/proxy.yaml:/etc/clausgap/proxy.yaml \
ghcr.io/clausgap/clausgap-proxy:latestPrivate beta. The proxy image and binary aren't publicly downloadable yet. Access comes with onboarding — email hello@clausgap.dev.
The image runs as an unprivileged user (uid 65532), not root. Mounted config only needs to be readable by it; the bundle cache and event spool default to /tmp. If you set bundle_cache_path or event_spool_dir to a volume, make it writable by uid 65532. On Kubernetes it runs under the restricted Pod Security standard:
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true # with an emptyDir mounted at /tmp
capabilities: { drop: ["ALL"] }
seccompProfile: { type: RuntimeDefault }Check liveness: GET http://localhost:8443/healthz returns {"status":"ok","proxy_id":"<id>"} right away. GET /readyz returns 503 {"status":"not_ready","reason":"no policies loaded"} until the proxy has fetched its first bundle, then 200 {"status":"ready","policies":<count>}.
If agents reach the proxy on a different address (a Docker port mapping, a Kubernetes Service, a load balancer), the proxy can't see it: addr is only where the binary listens. Set advertise_addr on the listener to the address agents use, and the Proxies page shows it as Agents connect to. It is a label only and changes nothing about how the proxy listens or decides. To confirm an address reaches the proxy you expect, call /healthz on it from where your agents run and compare proxy_id with the ID on the Proxies page.
This step completes when the proxy's first heartbeat arrives, within about 30 seconds of it starting. It then appears on the Proxies page.
Send your agent's traffic through it
Point your agent at the proxy's address in place of the API it calls today. The proxy needs to know which agent is calling. The fastest way is keyless mode — the proxy resolves identity from the Authorization header or source IP, with no agent code changes (see Keyless mode). For verified identity and confidence-level policies, add the X-Clausgap-Context HMAC-signed header — the SDK does it for you, or sign it yourself as below (each header covers one request and works once). See Context Signing for the full specification.
import base64, hashlib, hmac, json, secrets, time, requests
HMAC_SECRET = "my-secret" # matches proxy.yaml hmac_secret
PROXY_URL = "http://localhost:8443"
def make_context(method, target, body, agent_role, agent_id, session_id, confidence=1.0):
"""A v2 context header, signed for exactly this request and usable once."""
payload = json.dumps({
"agent_role": agent_role,
"agent_id": agent_id,
"session_id": session_id,
"confidence": confidence,
"step_index": 0,
"model_id": "gpt-4",
}).encode()
b64 = base64.b64encode(payload).decode()
ts = str(int(time.time()))
nonce = secrets.token_hex(16)
message = "\n".join([
"clausgap-context-v2", ts, nonce, method.upper(), target,
hashlib.sha256(body).hexdigest(), b64,
])
sig = hmac.new(HMAC_SECRET.encode(), message.encode(), hashlib.sha256).hexdigest()
return f"v2.{b64}.{sig}.{ts}.{nonce}"
# Sign the exact path + query you send — the header is only valid for it.
target = "/web_search?q=latest+sales+report"
resp = requests.get(
PROXY_URL + target,
headers={"X-Clausgap-Context": make_context("GET", target, b"", "my_agent", "a-1", "sess-001", 0.95)},
)
if resp.status_code == 200:
print("Allowed:", resp.json())
elif resp.status_code == 403:
print("Denied by policy")
elif resp.status_code == 423:
escrow_id = resp.json()["escrow_id"]
print("Held in escrow — awaiting M-of-N approval:", escrow_id)While Discovery Mode is on, a request from an agent with no policy is forwarded and recorded as observed, not denied, when a credential fixes the agent's role (an agent key, a token, an IP mapping or a role-bound signing key). In direct mode (a framework integration) that applies to agents using their own agent key, whose role the key fixes. This step completes on the first request, and you can watch them arrive on the Decisions page.
Review the policy drafted from your traffic
Once an agent has been observed, the Policies page lists its role with what it called and offers Generate draft. The draft has one allow rule for each tool and method the agent was seen using, and nothing more. Review it and edit it like any other policy: tighten a rule, add a deny, or hold a risky action for approval.
You can also write a policy from scratch, in the dashboard or with POST /v1/policies. The agent_role must match the role your agent is identified as.
policy "my_policy" {
description = "Describe what this policy controls"
agent_role = "my_agent"
environment = "staging"
allow call tool "read_data" when method = "GET"
allow call tool "submit_action"
deny call tool "delete_records"
# Hold dangerous actions for multi-party approval
escrow when tool = "submit_action" {
hold_type = "multi_party"
required_approvals = 2
notify = ["https://hooks.slack.com/services/T000/B000/approvals"]
}
# Hold low-confidence actions for review
escrow when confidence < 0.7
rate_limit 10 requests per minute
}
Turn enforcement on
A draft changes nothing until it is live. Open it and choose Deploy to proxy, or Submit for review if your workspace requires a second person to approve policy changes. The control plane compiles it and pushes a signed bundle to your proxy within one poll interval (default 30 s).
From then on, that agent's requests are decided by your policy: allowed, denied (403), or held for approval (423). The step completes on the first request a policy decides. Each decision is hashed, chained and stored as a forensic record.
Agents you haven't written a policy for keep passing through until Discovery Mode ends. Cover each one before then, or extend the window from Get started.
From here: give each agent its own credential (Connecting your identity provider), hold risky actions for approval (Escrow rules), and export evidence an auditor can check offline (Verifying evidence).
Proxy configuration reference
The unified proxy (clausgap-proxy) is configured via a single YAML file. A listeners: array activates one or more protocol listeners sharing a single policy engine and control-plane connection. All string values support $ENV_VAR substitution. See Protocol Reference for per-listener configuration fields.
| Field | Type | Description |
|---|---|---|
| tenant_id* | string | Your workspace tenant ID |
| proxy_id | string | Identifier for this proxy instance. Default: hostname |
| log.level | string | Nested under a top-level log: key. "debug", "info", "warn", "error". Default: "info" |
| listeners* | []Listener | Array of protocol listeners. Each entry has protocol (http | pg | mcp) and addr fields plus a protocol-specific sub-block. Each sub-block also accepts an optional tls: block (cert_file, key_file, client_ca_file for mTLS, min_version) — see Protocol Reference. |
| bundle.path | string | Local bundle file path (offline / air-gapped mode) |
| bundle.public_key_path | string | Ed25519 public key for bundle signature verification |
| control_plane.url | string | Control plane base URL |
| control_plane.api_key | string | API key used to fetch bundles and drain events |
| control_plane.poll_interval | duration | Bundle fetch interval. Default: "30s" |
| control_plane.bundle_cache_path | string | Where the last fetched bundle and the bundle-signing key are saved. A proxy restarted while the control plane is unreachable starts on them (still signature-, pin- and tenant-checked). Default: the temporary directory; set a volume path for it to survive a container being replaced. |
| control_plane.event_spool_dir | string | Where decision events wait when the control plane doesn't accept them, until it does; they are sent first, in order. At most 5,000 batches (oldest dropped first). Default: next to the bundle cache. |
| control_plane.public_key_sha256 | string | Pins the bundle-signing public key the proxy fetches from the control plane (hex SHA-256, logged by clausgap-control at startup). With a pin, a substituted key is refused over any connection; without one, the key is only fetched over https or from a private-network host. |
| trusted_proxies | []string | CIDRs of load balancers / reverse proxies in front of the proxy. Only connections from these may set the client address — needed behind a load balancer (e.g. Fly) for IP/CIDR identity, allowed_cidrs and per-IP rate limits to see the real client. |
| client_ip_header | string | Header those proxies set: "X-Forwarded-For" (default; the rightmost untrusted hop is the client) or a single-value header such as "Fly-Client-IP" or "X-Real-IP". Requires trusted_proxies. |
| scopes | []string | Policy scopes this proxy subscribes to. Default: ["default"]. Add named scopes to receive only matching policies from the control plane. |
| metrics.addr | string | Prometheus metrics bind address. Default: ":9090" |
Health endpoints
/healthz and /readyz are served on the HTTP listener regardless of policy load state. /metrics is served only on the separate metrics server bound to metrics.addr (default :9090), never on the port agents use: it carries agent roles and tool names. Keep that port off public networks.
/healthzLiveness check. Always returns 200 {"status":"ok"}.
/readyzReadiness check. 200 {"status":"ready","policies":<count>} once at least one policy bundle has loaded; 503 {"status":"not_ready","reason":"no policies loaded"} before that.
/metricsPrometheus metrics endpoint. On metrics.addr only.
Routes file
routes.yaml maps tool names (used in policy rules) to upstream services. The proxy routes each inbound request to the matching upstream after policy evaluation.
| Field | Type | Description |
|---|---|---|
| tool* | string | Name used in policy allow/deny/escrow rules |
| upstream* | string | Upstream URL. The request path is appended. |
| methods* | string[] | Allowed HTTP methods, e.g. ["GET","POST"] |
| match | string | "" (default): the upstream's path and everything below it, on whole path segments — /docs covers /docs/x but not /docsadmin. "prefix": a raw string prefix (e.g. /mcp_ matches /mcp_search). |
| protocol | string | "" for plain HTTP (default) or "jsonrpc" for MCP servers |
| discover | bool | When true + protocol=jsonrpc, calls tools/list on startup and creates per-tool routes automatically |
Requests whose path contains a . or .. segment — in any spelling, including percent-encoded and double-encoded — are refused with 400 INVALID_PATH before any policy is evaluated, so a path like /docs/../admin can't be judged as one tool and served by the upstream as another.
For MCP servers with discover: true, each discovered tool is registered by its bare tool name in policy rules, e.g. allow call tool "read_file".
Policy DSL
Policies are written in the Clausgap DSL — a human-readable, version-controlled configuration language. Each policy file contains one or more policy blocks.
Structure
policy "policy_name" {
description = "Human-readable description"
# Required — must match the agent_role in X-Clausgap-Context
agent_role = "my_agent"
# Optional — restrict to a specific environment
environment = "prod" # or "staging", "dev"
# Optional — policy scope for multi-proxy deployments (default: "default")
scope = "finance"
# ... rules ...
}Tool rules (HTTP proxy)
# Allow / deny by tool name
allow call tool "web_search"
deny call tool "delete_records"
# Wildcard
allow call tool "*"
# Conditional — only allow GET requests to this tool
allow call tool "read_data" when method = "GET"Table rules (PG proxy)
# Column-level read control
allow read on table "orders" columns ["id", "status", "total"]
deny read on table "users" columns ["ssn", "credit_card"]
# Blanket table control
allow read on table "products"
deny write on all tables
# Row-level filter — injected as WHERE clause on every query
filter on table "orders" where "region = 'EMEA'"Deterministic deny conditions (context_deny)
context_deny enforces hard denials on structured call metadata — SQL operations, table names, and time windows. These rules are deterministic: they do not depend on confidence scores or LLM output and are directly auditable in compliance reviews.
# Deny destructive SQL operations
context_deny operation in (DELETE, DROP, TRUNCATE)
# Deny access to sensitive tables
context_deny table in ("users_pii", "payments", "audit_log")
# Deny outside business hours (UTC)
context_deny time_of_day outside "09:00-17:00"context_deny rules run before allow checks and cannot be overridden by any allow rule. They always produce a hard 403 with the matched condition in the audit record.
Escrow rules
Escrow holds the request (423 Locked) until one or more approvers vote, an external condition is met, or a time delay expires. Supports four hold types: manual, multi_party, conditional, and time_delayed.
# Simple manual hold — one approver required
escrow when tool = "send_email"
# Multi-party — M-of-N approvers
escrow when tool = "submit_order" {
hold_type = "multi_party"
required_approvals = 2
notify = ["https://hooks.slack.com/services/T.../B.../finance"]
}
# Conditional — released when external webhook confirms
escrow when tool = "deploy" {
hold_type = "conditional"
condition = "webhook_confirm"
condition_url = "https://ci.internal/check"
}
# Manual with custom TTL (seconds)
escrow when tool = "modify_record" {
hold_type = "manual"
ttl = 86400
}
# Time-delayed — auto-releases after the delay unless cancelled
escrow when tool = "bulk_delete" {
hold_type = "time_delayed"
ttl = 3600
}Rate limiting and budgets
rate_limit 20 requests per minute
rate_limit 100 requests per hour
rate_limit 1000 requests per day
rate_limit 50 requests per session
# Not enforced yet: these compile with a warning and have no effect
cost_budget 5.00 per session # USD
token_budget 500000 tokens per hour
active_hours "08:00-20:00" timezone "Europe/London"
expires "2027-12-31T23:59:59Z"Evaluation order
When a request arrives, the proxy evaluates rules in this fixed order. Evaluation short-circuits at the first match. A rate_limit is enforced separately: a call over its limit gets 429.
Rate limits are shared: every proxy, every listener in it, and direct mode pool their counts through the control plane every couple of seconds, so an agent's limit holds however it reaches its tools. A burst split across paths can go over by what arrives in that time. Velocity limits and sequence rules are still counted by each proxy instance and separately in direct mode: run one proxy instance per tenant until they are shared too.
| # | Check | Outcome |
|---|---|---|
| 1 | Role + environment match | Find policies for this agent |
| 2 | Injection scan | Pattern matched in tool args → 403 |
| 3 | Sequence check | Suspicious tool sequence → 403 |
| 4 | context_deny check | Structured condition matched → 403 (hard, no override) |
| 5 | Velocity check | Call-rate spike detected → 403 |
| 6 | Deny check | Explicit deny rule matched → 403 (short-circuit) |
| 7 | Allow check | No matching allow rule → 403 (default deny) |
| 8 | Escrow check | Escrow condition met → 423 Locked (M-of-N voting) |
| 9 | Column masking | Denied columns returned as NULL (PG proxy) |
Injection Defense
Clausgap detects prompt injection and behavioural anomalies before your policy rules even run. All checks run inline, inside the proxy, and are configured in the same DSL as your allow/deny rules.
trigger_source — verify who initiated the call
Pass trigger_source in your SDK context to declare whether the tool call was initiated by a human user, another agent, or the environment (e.g. a retrieved document). Use it in when conditions to restrict sensitive tools to direct user intent only.
# Only allow email when the user — not a retrieved doc — triggered it
allow call tool "send_email" when trigger_source = "user"
deny call tool "send_email" when trigger_source = "environment"In the SDK, set triggerSource on each evaluate call. The proxy defaults to "user" when the field is absent.
// TypeScript SDK
await gate.evaluate({
toolName: "send_email",
triggerSource: "user", // "user" | "environment" | "agent"
})injection_scan — block known attack patterns
Scans tool arguments against a built-in library of prompt injection signatures (instruction overrides, privilege escalation, role impersonation, exfiltration attempts), plus any custom patterns registered for your tenant. Fires before rule matching; a tool can opt out with except injection_scan (below), but no other allow rule bypasses it.
injection_scan action = "deny"
# Opt a trusted tool out of scanning
allow call tool "internal_summariser" except injection_scanaction must be "deny" — it is the only value the validator accepts. An injection match is always a hard 403.
Custom patterns registered via POST /v1/admin/injection-patterns are compiled into the signed bundle and scanned alongside the built-in defaults — the built-in set is checked first, then your tenant's custom patterns.
sequence_deny — detect suspicious sequences
Triggers when a tool is called shortly after a specific preceding tool in the same session — a pattern common in exfiltration chains (read data, then immediately send it out). The within window is in seconds.
# Deny any exfil-like tool called within 60 s of a DB read
sequence_deny tools ["send_email", "post_webhook"]
after ["query_database", "read_file"]
within 60velocity_limit — spike detection
Flags sessions where the per-minute call rate spikes above a multiple of the session baseline. Unlike rate_limit (which enforces an absolute cap), velocity detection catches sudden bursts that are anomalous relative to normal behaviour.
# Deny if current-minute rate is 5× the session baseline
# (baseline established after min 10 calls)
velocity_limit spike 5.0 min 10 action = "deny"sequence_deny and velocity_limit both deny unconditionally when triggered today — there is no "escrow" action (only "deny" validates). Don't configure these expecting a human-review step — they hard-block the call.
Keyless mode (no SDK required)
Keyless mode lets you drop Clausgap in front of any existing agent framework — LangChain, AutoGen, CrewAI, raw tool loops — with zero code changes. The proxy resolves agent identity from the request itself using a priority resolver chain.
| # | Resolver | How it works |
|---|---|---|
| 1 | HMAC (SDK) | X-Clausgap-Context header — highest trust, verified identity |
| 2 | JWT Bearer | RS256/ES256/EdDSA token validated against JWKS endpoint; the provider must have an audience |
| 3 | API key | HMAC-SHA256 lookup against registered keyless API keys |
| 4 | IP / CIDR | Static role mapping — lowest priority, no credential required |
Enable keyless mode
# proxy.yaml
listeners:
- protocol: http
addr: ":8443"
http:
routes_file: "./routes.yaml"
keyless:
enabled: true
mode: "strict" # "permissive" allows anonymous fallback during migration
api_key_sources:
- header: "Authorization"
prefix: "Bearer "
- header: "X-API-Key"
static_roles:
- source_ip: "10.0.0.0/8"
agent_role: "internal-agent"
agent_id: "internal-01"
# Optional: enrich audit logs from advisory headers
supplementary_headers:
session_id: "X-Conversation-Id"
agent_id: "X-Agent-Instance-Id"
# Optional: extract developer-defined confidence from response body
confidence:
source: "body_json"
path: "$.confidence"
trust: "unverified" # advisory label: confidence came from an unsigned header/bodyIn strict mode, requests with no matching resolver are rejected with 403. Set mode: permissive with a permissive_until deadline to allow an anonymous fallback during initial rollout — the deadline IS the acknowledgement; once it passes, the proxy automatically enforces strict mode. Anonymous callers still can't reach a tool: no policy may name the role anonymous, and Discovery Mode never lets them through. Without keyless identity configured, a request with no credential and no matching IP role is refused.
Identity trust tiers
Every decision record includes an identity_resolver field that indicates how the agent was identified. The dashboard surfaces this as a trust badge on each event.
| Resolver | Trust tier | Audit label |
|---|---|---|
| hmac | Cryptographic | SDK-signed context header — tamper-evident |
| jwt | Cryptographic | RS256/ES256/EdDSA token, JWKS-verified |
| api_key | Cryptographic | HMAC-SHA256 registered credential |
| ip_cidr | Network-inferred | Source IP match — not a cryptographic assertion |
| anonymous | Unauthenticated | No resolver matched (permissive mode only) |
IP/CIDR identity is network-path inference, not a cryptographic assertion. Treat it as convenience for trusted internal networks only — not for regulated or high-value actions. Behind a load balancer the connection comes from the balancer, so set trusted_proxies and client_ip_header (see Proxy configuration) or every agent appears to come from the same address.
JWT providers
Register a provider with POST /v1/keyless/jwt-providers. An audience is required — it must match the aud claim your issuer puts in tokens meant for Clausgap; without it, any token the issuer minted for another application would be accepted. ES256, ES384 and ES512 keys must be on the curve the algorithm names (P-256, P-384, P-521), and the jwks_uri must be a public https URL. claims_mapping names the claim that carries the agent's role (default clausgap_role) and ID (default sub) — tokens from your organisation's identity provider work as they are. Registering a provider with a name or issuer you already registered is a 409. Setting this up end to end — with single sign-on for people too — is covered in Connecting your identity provider.
What reaches your upstream
The proxy removes the agent's Clausgap credentials before forwarding — the X-Clausgap-Context header and whichever header carried a keyless JWT or API key (Authorization, X-API-Key) — so a tool server can never replay them as the agent. To give a tool its own credentials, set upstream_headers on the HTTP listener (see Protocol Reference).
Policy scopes
Scopes let large deployments partition policies across proxy fleets — each proxy receives only the policies it needs. Every policy belongs to one scope (default: "default"). Proxies declare which scopes they subscribe to; the control plane compiles and signs a filtered bundle containing only matching policies. Existing deployments are unaffected — everything continues to use the implicit default scope.
Assign a scope in the policy DSL
policy "pii_guardrail" {
description = "PII controls for the finance team agents"
agent_role = "finance_agent"
scope = "finance" # ← assigns this policy to the "finance" scope
deny call tool "export_data"
context_deny table in (customers_pii, ssn_records)
}The scope field is optional. Omitting it is equivalent to scope = "default".
Subscribe a proxy to scopes
Add a scopes: list to proxy.yaml. The proxy sends this list in every heartbeat and uses it when fetching the bundle from the control plane.
tenant_id: ten_abc123
proxy_id: finance-proxy
# This proxy only receives policies tagged scope="finance" or scope="default"
scopes:
- default
- finance
control_plane:
url: https://api.clausgap.dev
api_key: $CLAUSGAP_API_KEY
listeners:
- protocol: http
addr: :8080
http:
routes_file: /etc/clausgap/routes.yamlHow it works end-to-end
Author tags the policy
Set scope = "finance" in the DSL. The control plane stores this alongside the policy and includes it in every compiled bundle.
Proxy declares subscriptions
The scopes: list in proxy.yaml is sent with every /v1/proxies/heartbeat and used as a query parameter when polling /v1/policies/bundle?scopes=default,finance.
Control plane filters
The bundle endpoint extracts only policies whose scope matches the requested list, re-signs the result with the tenant's Ed25519 key, and returns it.
Proxy enforces subset
The proxy loads the filtered bundle and evaluates every agent request against only the policies relevant to its role — finance policies never reach an HR proxy, and vice versa.
Evaluate Clausgap
Before connecting anything real, run the evaluations that come with the Clausgap source. Each starts its own local stack with synthetic data, runs scripted checks end to end, and writes a report you can keep as evidence. They're industry-neutral — the same checks run for finance, healthcare and DevOps examples.
| Evaluation | Type | Description |
|---|---|---|
| evaluation/kit | 54 checks | A sandbox with a test identity provider, a mail catcher, the dashboard, a webhook receiver and a sample SDK agent. Proves SSO sign-in with roles and domain verification, agent identity and spoofing, approval-gated policy changes and break-glass, webhook testing and retries, and the agent's approve-then-run-once flow. |
| evaluation/approvals | 46 checks | Two-person approval: a held action runs only after two different approvers, exactly once, only for the requesting agent and only as approved; rejection and expiry stop it. |
| evaluation/evidence | 26 checks | Evidence export verified offline with no network, again after a restart, and tampering — with the package or the database — detected. |
Private beta. The evaluation scripts live in the Clausgap source, which isn't public yet. Access comes with onboarding — email hello@clausgap.dev.
./evaluation/kit/run.sh # fresh kit, every scenario
./evaluation/kit/run.sh --up # keep it running to explore
./evaluation/approvals/run.sh
./evaluation/evidence/run.shProtocol Reference →
HTTP, PostgreSQL, and MCP listener config fields and examples.
API Reference →
Full REST API for policies, escrow, events, subscriptions, and users.
SDKs & Integration →
Direct HTTP integration, or framework adapters for LangChain, CrewAI, OpenAI, and more.
Deploy Proxy Wizard →
Generate routes.yaml, proxy.yaml, Docker Compose, and Helm values in minutes.