Checkpoint Docs
Enforce

KYA-OS Enforcement

Block AI agents at the edge until they present a cryptographic identity via HTTP Message Signatures or KYA-OS delegations

Overview

KYA-OS enforcement is a policy action (instruct) that refuses to serve a page to an AI agent until the agent presents a verifiable cryptographic identity. Instead of the best-effort detection-and-redirect approach (User-Agent pattern matching, TLS fingerprinting), this path uses a standards-based HTTP challenge: the gateway returns a 401 Unauthorized with structured instructions, and the agent must retry with a signed HTTP Message Signature (RFC 9421) or a KYA-OS delegation proof.

Unlike detection-based enforcement, KYA-OS enforcement is bypass-proof for agents that cannot forge a valid signature — the cryptographic identity check does not depend on User-Agent strings, IP ranges, or TLS fingerprints.

INSTRUCT is the fifth verdict, so it is documented here alongside the other four. The identity primitives it hands the agent off to — delegations, proofs, and the consent flow that mints them — live in Govern.

Use KYA-OS enforcement for auth-critical surfaces (payment APIs, data exports, admin actions) where false negatives in detection are unacceptable. Use detection-based enforcement for general content protection where a soft redirect to a consent page is sufficient.

Detection vs Identity

Checkpoint offers two tiers of agent enforcement. They are complementary and can be used on different endpoints of the same project.

Detection (redirect, block)Identity (instruct)
What it checksUser-Agent patterns, TLS fingerprints, IP rangesCryptographic signature (RFC 9421) or KYA-OS proof
Bypass-resistant?No — agents can evade with tool delegation or spoofYes — cannot forge a valid Ed25519 signature
Agent cooperationNot requiredRequired (agent must implement the protocol)
Catches unknown UAsYes (heuristic)No (unsigned agents fail the challenge)
Catches desktop appsPartial (TLS fingerprinting on gateway path only)Yes (if the app signs its requests)
Catches tool proxiesNo (third-party tools have their own fingerprints)Yes (the proxied request won't carry a valid proof)
Use casesMarketing pages, soft consent flows, content surfacesPayment APIs, data exports, admin actions

The Three-Round Flow

When an agent hits an endpoint configured with default_action: 'instruct', the gateway does not serve the page. Instead it returns a structured 401 challenge. The agent must obtain authorization out-of-band and retry with a signed proof.

Round 1 — Agent requests the protected resource

GET /protected HTTP/1.1
Host: api.customer.com
User-Agent: (ChatGPT, Claude, Comet, or any agent)

The gateway runs detection, looks up the project policy, sees default_action: 'instruct', and responds with a 401.

Round 2 — Gateway returns a structured challenge

HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: KYA realm="api", authorization_uri="https://kya.vouched.id/api/v1/bouncer/authorize?project_id=proj-xxx&..."
Link: <https://kya.vouched.id/api/v1/bouncer/authorize?...>; rel="kya-authorize", <https://kya.vouched.id/docs/enforce/kya-os-enforcement>; rel="help"
KYA-Action: instruct
KYA-Auth-Required: true
KYA-Auth-Url: https://kya.vouched.id/api/v1/bouncer/authorize?...
KYA-Project: proj-xxx
Cache-Control: no-store

{
  "message": "I'm unable to access api.customer.com on your behalf. This website requires you to install their app or extension before an AI assistant can access it.\n\nPlease visit the following link to set up access:\nhttps://kya.vouched.id/api/v1/bouncer/authorize?...\n\nOnce you have completed the setup, ask me to try again.",
  "user_action_required": {
    "action": "Install the app or extension for this website",
    "url": "https://kya.vouched.id/api/v1/bouncer/authorize?...",
    "reason": "api.customer.com has AI agent protection enabled and requires authentication before allowing AI access."
  },
  "mcp_i": {
    "version": "1.0",
    "action": "authenticate",
    "authorization_url": "https://kya.vouched.id/api/v1/bouncer/authorize?...",
    "project_id": "proj-xxx",
    "required_scopes": ["api:read"],
    "flow": {
      "type": "oauth2_delegation",
      "steps": [
        "1. Direct your user to the authorization_url",
        "2. User reviews requested scopes and grants consent",
        "3. Receive delegation credential (JWT)",
        "4. Include credential in KYA-Delegation header",
        "5. Retry this request with the proof"
      ]
    },
    "retry_instructions": {
      "header": "KYA-Delegation",
      "format": "JWT delegation credential from authorization flow"
    },
    "documentation": "https://kya.vouched.id/docs/enforce/kya-os-enforcement"
  },
  "error": "mcp_authentication_required",
  "code": "AGENT_REQUIRES_DELEGATION",
  "detection": {
    "agent_type": "ai_agent",
    "agent_name": "ChatGPT",
    "confidence": 95,
    "verification_method": "pattern"
  }
}

The response is engineered for two audiences:

  • Current LLMs (ChatGPT, Claude, Perplexity) read the message field as plain text and relay it to the user, who then completes the authorization flow in their browser.
  • KYA-OS-aware agents parse the mcp_i object programmatically, call authorization_url to obtain a delegation, and retry automatically.

The standard WWW-Authenticate: KYA header (RFC 7235) signals to HTTP clients that authentication is required and advertises the authorization endpoint. The Link header (RFC 8288) provides a discoverable relation to the KYA-OS docs.

The shape above is the legacy mcp_i challenge body. Gateway routes running the production-enabled draft-kya-http-02 §8.1 flow present a different retry contract: Authorization: Bearer <delegation> is the primary retry header, with KYA-Delegation documented as the fallback for clients where the Authorization header is already occupied by something else. Both forms are real and accepted — see Path C below.

Round 3 — Agent obtains a delegation and retries with a proof

The user (or the agent, if it is KYA-OS-capable) visits authorization_url, signs in, and reviews the requested scopes. On consent, Bouncer issues a delegation credential (a signed JWT) and returns it to the agent. The agent then retries the original request with cryptographic proof:

The gateway supports three retry mechanisms. Your choice depends on what kind of agent you are.

Path A — RFC 9421 HTTP Message Signatures (ChatGPT only):

GET /protected HTTP/1.1
Host: api.customer.com
User-Agent: ChatGPT-User/1.0
Signature: sig1=:<base64url-ed25519-signature>:
Signature-Input: sig1=("@method" "@path" "@authority" "host");created=1705123456;expires=1705123756;keyid="<public-key-id>"
Signature-Agent: "https://chatgpt.com"

The gateway parses the keyid from Signature-Input and resolves the key from ChatGPT's public directory: it fetches from /api/internal/signature-keys (a cached endpoint backed by a daily cron job pulling https://chatgpt.com/.well-known/http-message-signatures-directory), with hardcoded fallback keys in the worker for emergencies. Routing is keyed on Signature-Agent matching chatgpt.com.

It then verifies the Ed25519 signature over the canonicalized request base string (method, path, authority, host), validates the timestamp (±30s clock skew, 5-min max age), and on success upgrades the detection record to verificationMethod: 'signature' with 100% confidence.

This path is not available to KYA-OS agents. Signing an RFC 9421 request with a did:web:knowthat.ai:agents:<slug> keyid is not a supported flow and will fail verification — the gateway has no did:web resolver on its RFC 9421 branch. KYA-OS agents use Path B (the KYA-Agent-DID header trio) or Path C (a delegation proof), both of which resolve did:web correctly. Path B is the direct replacement if you were reaching for message signatures.

Path B — Simple DID+timestamp signatures (used only by KYA-OS agents, as an alternative to RFC 9421):

GET /protected HTTP/1.1
Host: api.customer.com
KYA-Agent-DID: did:web:knowthat.ai:agents:my-agent
KYA-Agent-Signature: <base64url-ed25519-signature>
KYA-Agent-Timestamp: 1705123456

This is a lighter-weight flow for agents that don't want to implement full RFC 9421. The Ed25519 signature covers the string ${did}:${timestamp} — the DID and the timestamp joined by a colon, not the timestamp alone and not the request. The gateway:

  1. Reads KYA-Agent-DID and resolves the public key: did:key DIDs carry the key inline (multibase Ed25519 multikey); did:web documents are fetched only from allowlisted hosts (knowthat.ai, kya.vouched.id, or *.agents.kya-os.ai) and cached for 5 minutes; managed *.agents.kya-os.ai agents fast-path the key from the routing table with zero network latency
  2. Verifies KYA-Agent-Signature over ${did}:${timestamp}
  3. Validates KYA-Agent-Timestamp (±30s clock skew, 5-min max age)
  4. On success, sets verificationMethod: 'did' with 100% confidence

Path C — KYA-OS delegation proof (delegation verified at the edge; scopes checked at the origin). The primary retry presentation is Authorization: Bearer <delegation> (per draft-kya-http-02 §8.1); KYA-Delegation is the documented fallback for clients whose Authorization header is already occupied by something else:

GET /protected HTTP/1.1
Host: api.customer.com
Authorization: Bearer eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
# Fallback presentation, when Authorization is unavailable to the client
GET /protected HTTP/1.1
Host: api.customer.com
KYA-Delegation: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...

The gateway verifies the delegation credential inline during detection — the delegation VC-JWT presented in either the Authorization: Bearer header or the KYA-Delegation fallback (part of the mcp_i.retry_instructions contract) is checked by the same engine that issued the challenge. When a gateway path rule declares required scopes, the edge enforces those too (a shortfall is refused with kyaos/scope-insufficient). Your per-endpoint scope requirements live in your origin's endpoint configuration, where @kya-os/bouncer-middleware checks the delegation's scopes. See Scopes and origin middleware.

What the origin receives after a successful verification

On the success path (a valid Path A or Path B signature, or a Path C delegation proof), the gateway forwards the request to your origin with these headers added:

Header (direction)Value
KYA-Project-Id (request)Your project UUID
KYA-Original-Origin (request)The original https://{hostname} the agent asked for
KYA-Request-Id (request & response)Per-request UUID for tracing

And the response headers the agent sees include:

Response headerExample valueMeaning
KYA-DetectedtrueWhether an agent was detected at all
KYA-Confidence100Confidence score
KYA-ActionallowPolicy action applied after verification
KYA-AgentChatGPTAgent name (if detected)
KYA-VerificationsignatureVerification path used (signature, did, or pattern)
KYA-TLS-Sourcerequest.cfWhere the TLS fingerprint came from
KYA-Duration-Ms12Gateway processing time

If verification fails, the gateway returns 401 again (or 403 if the agent is explicitly deny-listed by verified identity).

Deployment Architecture

The instruct action is supported on both the gateway path and the in-app middlewares, but the response they write differs. Next.js withCheckpointApi returns the 401 KYA-OS challenge by default (redirectMode: 'instruct'); the local-engine middlewares (Next.js withCheckpoint, Express) render the engine's Instruct decision as 422 application/problem+json; and the .NET module writes a 200, not a 401. See INSTRUCT by surface before writing a client that keys off the status code. The gateway path differs in where the work happens, not in capability: it needs zero code changes at your origin, it captures TLS fingerprints at the edge, and it challenges unverified traffic before that traffic ever reaches your infrastructure.

Three deployment topologies are supported:

Agent → DNS (CNAME to cname.checkpoint-gateway.ai) → Checkpoint Gateway → Your origin

Point a CNAME from your domain (e.g. api.acme.com) to cname.checkpoint-gateway.ai. The gateway extracts the hostname, looks up the project in its routing table, runs detection, enforces the policy, and proxies surviving requests to your origin. Your origin code runs unchanged.

See Gateway deployment for setup instructions.

2. Origin behind the gateway via custom DNS

If you cannot change the public DNS, route internal traffic through the gateway by pointing your load balancer at detect.checkpoint-gateway.ai and preserving the original hostname in the Host header.

3. In-app middleware only

Installing @kya-os/checkpoint-nextjs (or the Express or .NET equivalent) directly on your origin gives you detection and the instruct challenge with no DNS changes — withCheckpointApi emits the 401 KYA-OS challenge by default, and the local-engine middlewares render the engine's Instruct decisions.

What you give up relative to the gateway: TLS fingerprint capture (TLS terminates before your middleware runs), and edge filtering — the challenge is emitted by your application server, so unverified traffic still reaches your infrastructure before being challenged. See Middleware enforcement for setup.

Prerequisites

  • A Checkpoint project. See Credentials for how to find your Project ID and API key in Installations.
  • Traffic routed through the Gateway (recommended) or one of the in-app middlewares, so there's a surface to apply instruct to.
  • To enforce scopes at your origin: @kya-os/bouncer-middleware installed in front of the endpoints you're protecting — see Scopes and origin middleware.

Configuration

Setting the policy

Author a policy with an INSTRUCT verdict in Compose (natural language → @verdict("INSTRUCT")), or call PUT /api/internal/projects/{projectId}/policy directly. (The dashboard's Agent Traffic Handling card also exposes a project-wide default-action toggle, but Compose and the API below are the two paths this guide documents.)

COOKIE='authjs.session-token=<your-session-token>'
PID='<your-project-id-or-friendly-id>'

curl -X PUT "https://kya.vouched.id/api/internal/projects/$PID/policy" \
  -H "Content-Type: application/json" \
  -H "Cookie: $COOKIE" \
  -d '{
    "default_action": "instruct"
  }'

The response returns the updated policy. The gateway picks up the change after its 60-second policy-cache TTL expires (POLICY_CACHE_TTL_SECONDS, workers/gateway/src/detection/policy.ts), or immediately if GATEWAY_ADMIN_KEY is configured (the PUT route fires a best-effort cache invalidation). The 5-minute figure is the middleware policy-cache default (policyCacheTtlSeconds), not the gateway's.

Path-scoped enforcement

You can apply instruct to specific paths while leaving the rest of the site on redirect or allow. This is the recommended pattern: protect sensitive APIs with KYA-OS while letting detection handle marketing pages.

Path rules accept INSTRUCT: a rule carries pathPatterns, an action of INSTRUCT, and an optional instructConfig (docs URL, support contact, install commands) that customizes the challenge. instructConfig is accepted and stored by PUT /api/internal/projects/{projectId} /policy; the current Compose authoring surface doesn't expose an editor for it. Cedar policies authored in Compose can likewise scope an INSTRUCT verdict to specific resources.

Required scopes

The required_scopes field in the 401 response tells the agent what it needs in its delegation. Scopes are configured in the Tools section of the dashboard and enforced by @kya-os/bouncer-middleware at your origin; gateway path rules that declare required scopes are additionally enforced at the edge.

Scopes and origin middleware

The gateway handles the challenge, signature verification, and delegation verification at the edge, and enforces required scopes declared on its own path rules. Project-specific per-endpoint scope definitions live in your origin's endpoint configuration. The separation of concerns is:

LayerResponsibility
Gateway (detect.checkpoint-gateway.ai)Detects agents, returns the 401 challenge, verifies RFC 9421 signatures and delegation proofs
Bouncer middleware (origin)Verifies KYA-OS proofs (compact JWS at body._meta.proof.jws), checks scopes against requirements

To enforce scopes, install @kya-os/bouncer-middleware on your origin. This shows only the requiredScopes delta for a payments endpoint — see Proof Verification for the complete createBouncerMiddleware option reference:

npm install @kya-os/bouncer-middleware
import express from 'express';
import { createBouncerMiddleware } from '@kya-os/bouncer-middleware';

const app = express();

app.post(
  '/api/payments/create',
  createBouncerMiddleware({
    apiKey: process.env.AGENTSHIELD_API_KEY!,
    projectId: process.env.AGENTSHIELD_PROJECT_ID!,
    requiredScopes: ['payment:create'],
    reputationThreshold: 60,
  }),
  (req, res) => {
    const { agentDid, scopes, reputation } = req.bouncer;
    // ... handle the authorized agent request ...
  }
);

One API key, two env names: KYA-OS worker deploys and the bouncer-middleware convention use the legacy-named AGENTSHIELD_API_KEY, while the Checkpoint SDKs read CHECKPOINT_API_KEY — both hold the same dashboard API key. bouncer-middleware itself reads no environment variables; you pass every value explicitly, so use whichever name your deployment already defines.

See Proofs for the full proof verification flow.

Testing the flow

After enabling instruct on a project, verify the challenge is returned for agent traffic:

# Simulate ChatGPT browsing
curl -sI \
  -A "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot" \
  "https://api.acme.com/protected" \
  | grep -iE "(HTTP|WWW-Authenticate|KYA-)"

Expected output:

HTTP/2 401
WWW-Authenticate: KYA realm="api", authorization_uri="https://kya.vouched.id/api/v1/bouncer/authorize?..."
KYA-Action: instruct
KYA-Auth-Required: true
KYA-Auth-Url: https://kya.vouched.id/api/v1/bouncer/authorize?...
KYA-Project: proj-xxx

Then fetch the full JSON body:

curl -s \
  -A "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot" \
  "https://api.acme.com/protected" \
  | jq .mcp_i

This should show the full mcp_i object with authorization_url, required_scopes, and retry_instructions.

Limitations and known gaps

  • Edge filtering needs the gateway path. In-app middlewares emit the same instruct challenge, but they emit it from your application server — unverified traffic still reaches your infrastructure before being challenged. Only the gateway topology filters at the edge, and TLS fingerprint capture is gateway-only.
  • The gateway only enforces the scopes it knows about. It enforces the required scopes declared on its own path/challenge rules (a shortfall is refused with kyaos/scope-insufficient). It does not — and cannot — enforce your origin's own per-endpoint scope requirements, since those live in your origin's configuration, not the gateway's. Install @kya-os/bouncer-middleware at your origin to check those, so you get full KYA-OS coverage across both layers.
  • Non-cooperating agents are blocked, not redirected. If an agent does not implement RFC 9421 or KYA-OS proofs, it sees a 401 with no way to proceed. This is the intended behavior — use detection-based redirect if you want a graceful fallback for unsupported agents.
  • Agent tool delegation does not bypass this. Unlike detection, which can be evaded by asking an agent to proxy the request through a URL-inspection tool, KYA-OS enforcement requires cryptographic proof on the actual request hitting the gateway. A proxied request carries the proxy's identity, not the agent's, and fails the challenge.

Next Steps

  • Policies — Detection-based enforcement actions (allow/block/redirect)
  • Proofs — JWT proof verification at the origin
  • Delegations — Managing agent authorization grants
  • Consent — Customizing the authorization flow UI
  • Tools — Per-tool scope requirements
  • Gateway — Deploying traffic through the Checkpoint Gateway