Java
Pure-JVM AI agent detection for the JDK HttpServer and Servlet containers
Overview
Checkpoint for Java embeds the same kya-os-engine WASI build the .NET SDK and the DNS gateway run, and executes it through Chicory, a pure-Java WebAssembly runtime. There are no native dependencies, and it requires JDK 21 or later.
Every request is classified by the embedded engine, producing identical verdicts to the DNS gateway and the .NET and TypeScript SDKs (a cross-runtime parity check asserts this on every pull request), and detections report to the dashboard's /api/v1/log-detection with sdk.runtime: "jvm".
Prerequisites
- A Checkpoint project: get your Project ID and API key from Installations in the dashboard.
- JDK 21 or later.
- A
com.sun.net.httpserverbased app, or ajakarta.servletcontainer.
Installation
All three modules are on Maven Central. Add checkpoint-core and the adapter for your server; the adapter below is for a servlet container.
<dependency>
<groupId>ai.knowthat.checkpoint</groupId>
<artifactId>checkpoint-core</artifactId>
<version>0.2.1</version>
</dependency>
<dependency>
<groupId>ai.knowthat.checkpoint</groupId>
<artifactId>checkpoint-servlet</artifactId>
<version>0.2.1</version>
</dependency>For an app built on the JDK's com.sun.net.httpserver, use checkpoint-jdk-httpserver in place of checkpoint-servlet. The servlet API itself is provided: your container supplies it.
The thin jars resolve Chicory, ASM and Jackson onto your classpath. If your app already carries a different Jackson version, either pin Jackson through your own dependency management or use the adapter's shaded jar in place of both dependencies above. It contains the adapter, checkpoint-core and those libraries relocated under ai.knowthat.checkpoint.shaded.*, so it needs nothing else: exclude the transitive dependencies its pom still lists.
<dependency>
<groupId>ai.knowthat.checkpoint</groupId>
<artifactId>checkpoint-servlet</artifactId>
<version>0.2.1</version>
<classifier>shaded</classifier>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>Quick Setup
Checkpoint checkpoint = Checkpoint.create(CheckpointOptions.builder()
.projectId("<project-id>")
.apiKey("sk_live_...")
// The builder default is OBSERVE, which never blocks. Set ENFORCE to
// actually short-circuit.
.enforcementMode(CheckpointOptions.EnforcementMode.ENFORCE)
.onAgentDetected(EnforcementAction.BLOCK)
.build());
// jdk.httpserver:
context.getFilters().add(CheckpointFilter.wrap(checkpoint));
// servlet containers:
new CheckpointServletFilter(checkpoint);Call checkpoint.warmup() during your app's own startup to pre-warm the engine pool off the request path; both adapters already call it synchronously during their own init, so size your container's health-check timeout accordingly.
Enforce vs Observe Mode
enforcementMode defaults to OBSERVE, which never short-circuits a request: every response the engine decided carries KYA-Mode: observe, and whatever onAgentDetected (or the dashboard policy) would have done, the request passes through with KYA-Would-Have-Been: <block|redirect|instruct|challenge> and KYA-Would-Have-Been-Reason: <threshold|deny_list|configured_action|...>. The telemetry event reports enforcement.action: "log" with the withheld action in enforcement.wouldBlock. Set ENFORCE to actually block, redirect, instruct, or challenge.
onAgentDetected defaults to EnforcementAction.LOG and confidenceThreshold defaults to 70.
Client IP: Trust Profiles
The client IP the engine sees is the direct TCP peer by default, which is the load balancer's own address once anything sits in front of the app: the engine's Tier-2 vendor-IP cross-match then never fires against the real caller. Both adapters ship the same Direct/Vercel/Cloudflare/TrustedProxy header-precedence profiles the TypeScript and .NET SDKs have:
checkpoint-servlet:CheckpointServletFilter's two-argument constructor takes aClientIpTrustPolicy, or configure it via theclientIpPolicy(vercel/cloudflare/trusted-proxy) andclientIpTrustedHops(hop count fortrusted-proxy) filter init-params. Default staysdirectPeer().checkpoint-jdk-httpserver:RemoteIpResolver.vercel(),.cloudflare(), and.trustedProxy(int hops)factories, passed toCheckpointFilter.create/.wrapthe same waydirectPeer()always was. A fifth, Checkpoint-specific profile,RemoteIpResolver.gateway(), trusts the gateway'sKYA-Client-IPheader for an origin deployed directly behind the Checkpoint Cloudflare gateway; it additionally verifies the direct peer against Cloudflare's published edge IPv4 ranges before trusting the header at all.
Every profile is opt-in, and none of them is safe to select on an origin any untrusted caller can also reach directly. Either adapter also accepts a fully custom RemoteIpResolver / ClientIpResolver for a topology none of the built-in profiles cover.
Response Headers
Every response includes detection metadata:
If a burst of traffic exhausts the bounded WASM engine pool (maxEngineInstances, engineAcquireTimeout), the request degrades to detection-unavailable rather than blocking on a cold instantiation: KYA-Detection: unavailable is stamped, and under failOpen (the default) the request still passes through with KYA-Failopen: true.
Signature and Delegation Verification
ChatGPT's RFC 9421 HTTP Message Signatures are verified whenever a request carries Signature / Signature-Input headers: the SDK reconstructs the signature base and verifies it with the JDK's native Ed25519 support against a live-fetched public key, no configuration required. A verified signature yields 100% confidence.
Separately, a request that presents a delegation credential (Authorization: Bearer <VC-JWT>, or the KYA-Delegation marker header for hosts that strip Authorization) with no kya-http/2 signature routes to the engine's standalone delegation-verify entry instead of plain detection. Unlike the .NET SDK's live did:web resolution, the Java SDK resolves trusted delegation roots, DID documents, and revocation lists from static, operator-configured maps on CheckpointOptions rather than fetching them live.
Dashboard Reporting
Detection events are posted to POST /api/v1/log-detection as a batch, fire-and-forget after the request completes; pure human traffic is not reported. The first verdict after startup, and the first after each flush, is sent immediately. Later verdicts are buffered until telemetryBatchSize (default 50) is reached, you call Checkpoint.flush(), or the Checkpoint closes; each Checkpoint also registers a JVM shutdown hook until it is closed, so create one per application and close it. A batch that fails with a transport error, 429, 502, 503 or 504 is retried on the next of those triggers, up to 3 attempts per batch, and close() or the shutdown hook make one last attempt before the batch is dropped with a rate-limited warning. Nothing flushes on a timer. Requires projectId and apiKey to be set: without them, detection and enforcement still run, only the dashboard hop is skipped. Raw IP and full URLs are reported by default; .telemetryHashIp(true) on the builder salts the IP with a project-scoped SHA-256 hash before it leaves the process, and .telemetryStripQuery(true) strips the ?query suffix from reported paths and URLs.
Browser posture outcome
With browser posture configured, the filters verify the posture token once per request. A handler reads that outcome instead of verifying the token again: the request attribute PostureOutcome.REQUEST_ATTRIBUTE (ai.knowthat.checkpoint.postureOutcome) with CheckpointServletFilter, and CheckpointFilter.postureOutcome(exchange) with CheckpointFilter.
A token can also carry a detection claim: the beacon's own final verdict, {class: "ai_agent" | "bot", name, confidence}. When the SDK's baseline is Human or IncompleteData, the claim classifies the request as that class with that name and confidence (an AI agent or bot, isAgent true), and it wins over the automation flags and the agent-driven verdict. An existing agent or bot verdict is never downgraded. A present but malformed claim (a class other than ai_agent or bot, a name outside 1 to 64 characters or with control characters, a confidence that is not an integer from 0 to 100) rejects the whole token as INVALID_CLAIMS. A disclosed navigator.webdriver now also sets isAgent true on its Bot classification, matching the TypeScript SDK.
Block responses
A blocked request gets a JSON body at blockStatusCode (default 403). To answer in plain text instead, set .responseFormat(CheckpointOptions.AgentResponseFormat.PLAIN_TEXT). An ordinary agent block then returns 200, since AI fetchers tend to drop the body of a 4xx response, with a plain-text AI Agent Notice that points to your redirectUrl (your MCP server) when that is an absolute URL. A request whose presented credential fails verification still gets blockStatusCode in either format, with its kyaos/* code in the KYA-Block-Reason header.
