A runtime trusted-contract gate for enterprise agent tools. Each tool call is evaluated against a versioned ToolContract derived from an approved OpenAPI artifact. The gate blocks or escalates calls when the live tool surface drifts from the approved contract.
All decisions are deterministic rule-based logic — no LLM calls in the gate path, no cloud dependencies.
| Module | Role |
|---|---|
gateway/ |
Spring Boot 3 / Java 21 gateway. Loads YAML contracts at startup, parses HS256 JWTs, runs the validation pipeline. |
agent-runner/ |
Java CLI. Mints a JWT, posts to the gateway, prints the response. Also drives the experiment harness. |
openapi/approved/ |
OpenAPI 3.1 artifacts that contracts derive from. |
deploy/ |
Kubernetes manifests + kind cluster config. |
experiments/ |
Case set, harness script, committed results.csv. |
Prereqs: Java 21, Maven 3.9+, Docker, kind 0.31+, kubectl 1.30+.
make demo-up # one-shot: kind cluster + image + deploy + jars
make port-forward # in one shell — forwards localhost:8081
make logs # in another — streams gateway logs
./demo/run.sh happy # 200 OK
./demo/run.sh mismatch # 403 contract_tool_mismatch
./demo/run.sh drift # 409 contract_version_drift
./demo/run.sh schema # 400 schema_violation
make demo-down # tear downLocal fallback (no Kubernetes):
make run-local # starts gateway on :8081
./demo/run.sh happymvn -pl gateway test # all gateway tests
mvn -pl gateway -Dtest=ToolContractGateTest test # one class
mvn -pl gateway -Dtest=ToolContractGateTest#shadowAlwaysAllows test # one method{
"mode": "TOOL_CONTRACT_GATE",
"contract": {"id": "catalog.search", "version": "v1"},
"tool": "catalog.search",
"scopes": ["catalog.read"],
"payload": {"query": "term"},
"side_effect_level": "read_only",
"requires_confirmation": false,
"audit_class": "internal",
"checks": {
"version_check": true,
"schema_check": true,
"provenance_check": true,
"scope_check": true,
"annotation_check": true,
"warn_only": false
}
}Response:
{
"decision": "ALLOW",
"runtimeDecision": "ALLOW",
"mode": "TOOL_CONTRACT_GATE",
"gateLatencyMs": 1,
"contractId": "catalog.search",
"expectedVersion": "v1",
"requestedVersion": "v1",
"tool": "catalog.search",
"reasonCode": null,
"violations": []
}Adds an Authorization: Bearer <jwt> header, runs the full pipeline plus audit logging, and returns HTTP 200/400/403/404/409 with detailed error bodies.
| Mode | Behavior |
|---|---|
NO_GATE |
Returns ALLOW unconditionally. Control baseline. |
SCHEMA_ONLY |
Only JSON Schema validation runs. |
TOOL_CONTRACT_GATE |
Full pipeline. Can block. |
SHADOW_GATE |
Full pipeline runs, but runtimeDecision is forced to ALLOW. Use for shadow-mode production rollout. |
The checks block on /v1/decide can disable individual stages: version_check, schema_check, provenance_check, scope_check, annotation_check. The warn_only flag downgrades every BLOCK or REVIEW to WARN. Severity precedence is BLOCK > REVIEW > WARN > ALLOW.
Contracts live in gateway/src/main/resources/contracts/*.yaml and load at startup.
id: catalog.search
version: v1
tool: catalog.search
owner: catalog-platform
audit_class: internal
required_scopes: [catalog.read]
limits:
max_payload_bytes: 4096
max_rps: 50
side_effect_level: read_only # read_only | write | destructive
requires_confirmation: false
idempotent: true
compatibility:
allowed_versions: [v1]
drift_policy:
version_mismatch: block
tool_mismatch: block
schema_violation: block
scope_violation: block
side_effect_change: block
confirmation_change: block
limit_change: warn
audit_class_change: review
provenance:
source_type: openapi
source_path: openapi/approved/catalog.yaml
openapi_sha256: "0aaf9e9b..."
operation_id: searchCatalog
generated_by: manual-seed
generator_version: "0.1.0"
annotations:
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
input_schema:
type: object
required: [query]
properties:
query: { type: string }
page: { type: integer }openapi_sha256 is verified at boot against the referenced OpenAPI artifact. If the SHA-256 does not match, that contract returns BLOCK on every call.
Re-run the benchmark and regenerate experiments/results.csv:
./experiments/run-experiments.shThe script auto-builds both jars if missing, starts the gateway locally if :8081 is free, runs the case set across all modes and ablations, and writes the CSV.
flowchart LR
A[Agent Tool Request] --> B[ToolContractGate]
C[Contract Registry] --> B
P[Approved OpenAPI artifacts] -. SHA-256 verify .-> C
B -->|ALLOW| E[Tool Execution]
B -->|WARN| E
B -->|REVIEW| F[Human Review]
B -->|BLOCK| G[Blocked]
B -. SHADOW_GATE .-> S[Shadow log]
B --> H[Audit Log]
make help
build Build all jars
agent-build Build only the agent-runner shaded jar
image Build the gateway docker image
kind-up Create the kind cluster (idempotent)
kind-load Load the gateway image into the kind cluster
deploy Apply k8s manifests
wait Wait for the gateway pod to be ready
demo-up One-shot: cluster + image + deploy + jars
demo-down Tear down the kind cluster
port-forward Forward gateway to localhost:8081
logs Stream gateway logs
run-local Run the gateway directly (no k8s)
clean Remove build artifacts
- Pod stays Pending. Docker Desktop may be low on resources. Bump CPU/memory in Docker settings, then
make demo-down && make demo-up. - Image not found in kind. Re-run
kind load docker-image contract-gateway:0.1 --name agent-gateway. Pin the tag — never:latest. - Port 8081 already bound.
lsof -i :8081 -tto find the holder. - JWT signature errors. The gateway secret (Kubernetes
gateway-jwt) and the agent-runner default must match. The runner readsGATEWAY_JWT_SECRETfrom env; if you change the Kubernetes secret, export the same value before running.
MIT. See LICENSE.