Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ToolContractGate

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.

Modules

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.

Quick start

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 down

Local fallback (no Kubernetes):

make run-local            # starts gateway on :8081
./demo/run.sh happy

Tests

mvn -pl gateway test                                                # all gateway tests
mvn -pl gateway -Dtest=ToolContractGateTest test                    # one class
mvn -pl gateway -Dtest=ToolContractGateTest#shadowAlwaysAllows test # one method

API

POST /v1/decide — gate-only path (always returns HTTP 200)

{
  "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": []
}

POST /v1/invoke — demo path

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.

Modes

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.

Per-call ablation flags

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.

Writing a contract

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.

Experiments

Re-run the benchmark and regenerate experiments/results.csv:

./experiments/run-experiments.sh

The 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.

Architecture

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]
Loading

Make targets

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

Troubleshooting

  • 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 -t to find the holder.
  • JWT signature errors. The gateway secret (Kubernetes gateway-jwt) and the agent-runner default must match. The runner reads GATEWAY_JWT_SECRET from env; if you change the Kubernetes secret, export the same value before running.

License

MIT. See LICENSE.

About

Deterministic governance gate for AI agent tool calls — validates every call against a versioned, approved contract (allow / warn / review / block) with no LLM in the decision path.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages