Integrations

.NET

AI agent detection for ASP.NET Core and .NET Framework

Overview

Vouched KYA for .NET works on ASP.NET Core (.NET 8, 9 and 10) and .NET Framework (4.6.2 through 4.8.1). One package, pick your runtime, ship.

Prerequisites

  • A Vouched KYA project: get your Project ID and API key from Installations in the dashboard.
  • An ASP.NET Core (.NET 8, 9 or 10) or .NET Framework (4.6.2โ€“4.8.1) application. .NET Framework requires a 64-bit host process: see Troubleshooting.

Installation

dotnet add package KyaOs.Checkpoint

NuGet resolves the right adapter for your project's target framework automatically.

Quick Setup

Set credentials in appsettings.json:

{
  "Checkpoint": {
    "ProjectId": "your_project_id",
    "ApiKey": "your_api_key"
  }
}

Then register in Program.cs, which reads those two settings:

using Checkpoint.AspNetCore.Extensions;
using Checkpoint.Core.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCheckpoint(options =>
{
    options.ProjectId = builder.Configuration["Checkpoint:ProjectId"];
    options.ApiKey = builder.Configuration["Checkpoint:ApiKey"];
    options.OnAgentDetected = DetectedAction.Block;
});

var app = builder.Build();

app.UseCheckpoint();           // before routing + auth
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();

If either setting is missing, the app still starts, in local detection-only mode: detection and your OnAgentDetected action run, but no dashboard policy applies and no detections reach the dashboard. Vouched KYA logs one warning at startup that names the missing setting. An ApiKey without a ProjectId does nothing, so that case is logged as an error. An empty value counts as missing.

Deploy. Vouched KYA runs on every request.

Configuration

builder.Services.AddCheckpoint(options =>
{
    options.ProjectId = "your_project_id";
    options.ApiKey = "your_api_key";

    options.OnAgentDetected = DetectedAction.Block;
    options.ConfidenceThreshold = 70;
    options.SkipPaths = ["/health", "/api/webhooks/*"];
    options.IncludePaths = ["/api/*"];

    options.McpServerUrl = "https://mcp.example.com";
    options.SiteName = "My Website";

    options.BlockedResponse = new BlockedResponseOptions
    {
        StatusCode = 403,
        Message = "Access denied: AI agent detected",
    };
});

Detection Actions

ActionBehavior
AllowLet the request through silently
LogLog and allow (observe mode, see Enforce vs Observe Mode)
BlockASP.NET Core: 200 with a plain-text notice for the agent in place of the page by default (AgentResponseFormat = PlainText); set AgentResponseFormat to Json to return BlockedResponse.StatusCode (403 by default) with a JSON body. .NET Framework: BlockedResponse.StatusCode with a JSON body
Redirect302 to McpServerUrl
InstructHTTP 200 (always, on .NET) with a JSON body telling the agent where to send its user to authorize, then how to retry (see KYA-OS Instruct Mode)
Challenge401 delegation challenge for consent/identity: requires AuthorizationHostUrl + ProjectId

Path Filtering

IncludePaths takes priority: if set, only matching paths are processed. Both support wildcard suffixes (/api/*).

Enforce vs Observe Mode

Vouched KYA for .NET picks observe or enforce with OnAgentDetected. DetectedAction.Log is observe mode and the default, so a fresh install starts in observe mode: every request is still classified, but only logged and reported to the dashboard as log, and the SDK blocks nothing on its own. Once ProjectId and ApiKey are set, the project's dashboard policy still applies: an agent on its deny list is blocked even under Log, so keep the deny list empty while you observe. DetectedAction.Allow also passes every request through, but reports as allow (an enforce-mode permit) rather than an observe-mode log. Block, Redirect, Challenge, and Instruct are the enforcement actions.

A request that presents a KYA-OS HTTP signature is refused in every mode, observe included, when the SDK can't verify the signature. That covers a signed body over 1 MiB (see Signature Verification). Java's observe mode passes such a request through and reports it instead; .NET has no equivalent.

Access Detection Results

Vouched KYA attaches its detection result to each request. Read it in controllers or downstream middleware with GetCheckpointResult(), not by its HttpContext.Items key: the key is an internal name and can change between releases.

using Checkpoint.AspNetCore;

public IActionResult Get()
{
    var result = HttpContext.GetCheckpointResult();

    if (result?.IsAgent == true)
    {
        return Ok(new { agent = result.DetectedAgent?.Name, confidence = result.Confidence });
    }

    return Ok(new { message = "Hello human!" });
}

GetCheckpointResult() returns null when the middleware produced no verdict for the request: it is not in the pipeline, or it failed open on an error before reaching a verdict. On a path the middleware skips (outside IncludePaths, or in SkipPaths) it returns a placeholder human result instead, and WasCheckpointSkipped() returns true; check it before you gate anything on the result. GetCheckpointSessionId() returns the session id the middleware resolved.

Read the browser posture outcome

With browser posture configured, the middleware or module verifies the posture token once per request. A handler reads that outcome instead of verifying the token again. GetCheckpointPostureOutcome() returns a PostureOutcome? (namespace Checkpoint.Core.Http): StatusName is verified, absent, or rejected, ReasonName names why a token was rejected, and Posture holds the verified claims when the status is verified.

using Checkpoint.AspNetCore;

app.MapGet("/checkout", (HttpContext context) =>
{
    var outcome = context.GetCheckpointPostureOutcome();

    if (outcome is null)
    {
        return Results.Ok(new { posture = "unknown" });
    }

    return Results.Ok(new
    {
        posture = outcome.StatusName,
        reason = outcome.ReasonName,
        verdict = outcome.Posture?.Verdict,
    });
});

The extension class is CheckpointAspNetCoreHttpContextExtensions. Its name differs from the classic CheckpointHttpContextExtensions so both packages can be imported in one file.

null means the request has no verdict: the middleware or module skipped it, didn't run, or browser posture isn't configured. It isn't the same as absent, which means the request carried no token. A handler that gates an action on posture should treat null as unknown, not as absent. A rejected or absent token doesn't stop the request, so the handler decides what to do with it.

A token can also carry a detection claim: the beacon's own final verdict, { class: "ai_agent" | "bot", name, confidence }, exposed as Posture.Detection (Class, Name, Confidence). When the baseline detection is human or incomplete data, 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 with ReasonName invalid-claims. A disclosed navigator.webdriver now also sets IsAgent true on its Bot classification, matching the TypeScript SDK. The detection claim and a disclosed navigator.webdriver set IsAgent true, so your configured OnAgentDetected action applies to them when the confidence meets ConfidenceThreshold. Corroborated-only automation evidence classifies the request as incomplete data with IsAgent false, so it never enforces. The agent-driven verdict never runs the configured action; it is reported and can only trigger a browser integrity step-up.

Response Headers

Every response includes detection metadata:

HeaderValueWhen
KYA-Detectedtrue / falseAlways
KYA-Confidence0โ€“100Always
KYA-ClassHuman / AiAgent / BotAlways
KYA-AgentAgent nameWhen detected
KYA-VerificationVerification methodWhen known

KYA-Verification names the method behind the classification: Pattern, Signature, Behavioral, Network, KyaHttp, KyaHttpDelegated, McpIHandshake or A2A.

Signature Verification

Ed25519 signature verification for ChatGPT (RFC 9421) is on by default. Verified agents score at the top of the confidence range. ChatGPT's public key refreshes automatically from OpenAI's JWKS.

Setting EnableSignatureVerification to false turns off the ChatGPT check only. KYA-OS signatures are verified either way.

To verify a KYA-OS signature, the SDK reads the request body and checks it against the signed Content-Digest, then hands your app the same bytes. A signed body may be up to 1 MiB. A larger one is answered 413 in every mode. When Content-Length declares the size, the SDK answers before reading any of the body. A chunked body is read only up to the limit.

options.EnableSignatureVerification = false;

KYA-OS Instruct Mode

DetectedAction.Instruct returns a KYA-OS challenge carrying a machine-readable mcp_i JSON body and a plain-language message the agent can relay to its user.

.NET writes HTTP 200 for Instruct, unconditionally. Unlike the Node SDKs it does not negotiate an envelope for this action: DelegationChallengeMode is honoured for Challenge only. A client that treats 2xx as success will read every instruct response as a successful request, so branch on the mcp_i body or the KYA-Action: instruct header, never on the status code. See INSTRUCT by surface for how the other four surfaces differ.

Both runtimes send the same response. message and user_action_required are plain text an AI assistant can relay to its user. mcp_i is for clients that drive the flow themselves: send the user to authorization_url to grant consent, then retry the request with the delegation credential in the KYA-Delegation header.

For example, ChatGPT asks a site whose ProjectId is your_project_id for a page:

GET /products HTTP/1.1
Host: shop.example.com
User-Agent: Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://chatgpt.com/bot)

Vouched KYA answers in place of the page:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
KYA-Action: instruct
KYA-Auth-Required: true
KYA-Auth-Url: https://kya.vouched.id/connect/your_project_id?agent=chatgpt
KYA-Project: your_project_id
{
  "message": "This website requires AI agent verification. To give me access, please visit: ...",
  "user_action_required": { "action": "Set up the MCP connection for this website", "url": "..." },
  "mcp_i": {
    "version": "1.0",
    "action": "authenticate",
    "authorization_url": "https://kya.vouched.id/connect/your_project_id?agent=chatgpt",
    "project_id": "your_project_id",
    "required_scopes": ["api:read"],
    "retry_instructions": {
      "header": "KYA-Delegation",
      "format": "JWT delegation credential from authorization flow"
    }
  },
  "error": "mcp_authentication_required",
  "code": "AGENT_REQUIRES_DELEGATION"
}

The response is abbreviated: it leaves out the Link header, the detection headers listed under Response Headers, and some body fields, among them mcp_i.flow, which lists the same steps in words. When a dashboard policy sets a redirect URL, authorization_url is that URL with ?agent= added. Otherwise it is your project's connect page, {BaseUrl}/connect/{ProjectId}?agent=<agent name, lowercased>, where the user installs the connection for their assistant.

Once the user has granted consent, the agent sends the same request again with the delegation credential:

GET /products HTTP/1.1
Host: shop.example.com
User-Agent: Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://chatgpt.com/bot)
KYA-Delegation: <delegation credential from the authorization flow>

To turn it on:

options.OnAgentDetected = DetectedAction.Instruct;
options.McpServerUrl = "https://mcp.example.com";

Native HTTP Mode

In native mode the module registers each challenge with Vouched KYA, which relays it to your project's authorization host. Without the native settings, the module registers challenges directly with the authorization host, using your site's API key.

Native mode arrived in 1.10.0. Use the current release, 1.15.0.

SettingValue
HttpResourceUriYour site's exact HTTPS resource URI, including any path: https://www.example.com
HttpResourceDidYour site's DID: did:web:www.example.com
HttpChallengeSigningDidsYour organization's DID: did:web:kya.vouched.id:orgs:<org-slug>
TrustedDelegationRootsYour organization's DID again
options.HttpResourceUri = "https://www.example.com";
options.HttpResourceDid = "did:web:www.example.com";
options.HttpChallengeSigningDids = ["did:web:kya.vouched.id:orgs:acme-corp"];
options.TrustedDelegationRoots = ["did:web:kya.vouched.id:orgs:acme-corp"];
  • The organization DID comes from your organization's slug, not its display name. Renaming the organization in the dashboard leaves the slug and the DID unchanged. If the slug itself changes, so does the DID. Vouched KYA has done this once, to remove generated suffixes such as -abc123. The old DID keeps resolving, but new challenges are signed under the new one, and the module refuses them until you update HttpChallengeSigningDids and TrustedDelegationRoots on every site. If you use the shopper helper below, update CustomerRoot too: with the old DID, every shopper is served as a guest, with no error. To check the value, open https://kya.vouched.id/orgs/<org-slug>/did.json: its id is the DID to use.
  • The module fetches the DID documents and status lists a credential needs, in the background. A request never waits on a fetch: one not fetched yet is answered 503 kyaos/identity-pending or kyaos/status-pending with Retry-After: 1, and the client's retry usually verifies. Fetches are HTTPS on port 443 to public addresses only, so the server needs outbound HTTPS to kya.vouched.id and to the did:web hosts of the agents you accept.
  • FailOpen defaults to false. Native authorization already fails closed on a missing or invalid credential. With native mode on and FailOpen = false, the module doesn't fall back to its last cached policy: every request, humans included, gets a 503 kyaos/policy-unavailable whenever the policy can't be fetched. Set FailOpen = true to fall back to the cached policy.
  • Leave AuthorizationHostUrl as it is. In native mode the module registers each challenge with Vouched KYA (BaseUrl, https://kya.vouched.id by default) and takes the consent link from that registration. The authorization host in your published policy wins over this setting; one of the two only has to be present for registration to run.
  • Sign-in callback. If your site is also the sign-in provider for consent, its OAuth app needs the authorization host's callback among its allowed redirect URIs. When consent runs on Vouched KYA, it's https://kya.vouched.id/api/consent/oauth/callback. The /api/v1/bouncer/oauth/callback in the Govern docs is for GitHub- or Google-style providers, not your own sign-in.
  • Confirm it took effect. After deploying, open https://www.example.com/.well-known/oauth-protected-resource. resource should be your HttpResourceUri, and kyaos_challenge_signing_dids should list your organization DID. If the document has no kyaos_challenge_signing_dids, the site isn't running native mode.

Act as the Shopper on a Delegated Request

Once a shopper approves an agent, the agent retries with a delegation credential and the module verifies it. That request still arrives without the shopper's cookies, so a page that builds the cart or the account from the session sees a guest. To serve the shopper's own cart, read who approved the grant from the detection result and load that customer.

The module sets DelegationHolder on the result only after it has verified a delegation credential (VerificationMethod is KyaHttpDelegated). It is null on every other request.

FieldHolds
SubjectThe shopper's DID: did:web:kya.vouched.id:orgs:<org-slug>:users:<id> when Vouched KYA issued the grant
CustomerIdThe <id> from Subject, when the grant's issuer is in your TrustedDelegationRoots. Otherwise null. Added in 1.15.0
IdentifierThe shopper's email from the grant, when it has one

When <id> is your customer id. It is the id your own sign-in returned for that shopper when the shopper signed in on the consent page with your sign-in: your OAuth provider or a merchant credential, set under Policy โ†’ Auth. For an OAuth provider that is the userinfo field the provider's user id mapping names (userInfoFields.userId), or the first of sub, id and customerId when no mapping is set. With Vouched KYA's sign-in methods (email code, passkey, password, a Vouched KYA account) it is an id Vouched KYA assigned, not one of your customers. A Subject that is a did:key has no customer id at all: it comes from a grant another authorization host issued, so treat that request as a guest or refuse it.

To act as the shopper:

  1. Check that VerificationMethod is KyaHttpDelegated and that the policy didn't block the request (ShouldBlock is not true).
  2. Check that GrantedScopes includes the scope this endpoint needs, and refuse with 403 when it doesn't. Each endpoint names its own scope: cart:read to view the cart, cart:write to add to it. A grant to view the cart can't add to it.
  3. Read DelegationHolder.CustomerId, and check that Subject is exactly <your organization's DID>:users:<CustomerId>. CustomerId is set for a grant from any of your TrustedDelegationRoots, and it doesn't say which: user 42 from each root gives the same CustomerId. Subject tells them apart. null, or a Subject that doesn't match, means no customer: don't act as anyone.
  4. Load that customer's cart instead of the session's.

The helper below does the first three steps, so every endpoint makes one call with its own scope.

using Checkpoint.AspNetCore;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

private const string CustomerRoot = "did:web:kya.vouched.id:orgs:acme-corp";

public IActionResult Cart()
{
    var access = DelegatedShopper.Check(
        HttpContext.GetCheckpointResult(), CustomerRoot, "cart:read", out var customerId);
    if (access == ShopperAccess.Forbidden)
    {
        return StatusCode(StatusCodes.Status403Forbidden);
    }

    return View(access == ShopperAccess.Shopper
        ? _carts.ForCustomer(customerId)
        : _carts.ForSession(HttpContext));
}

[HttpPost]
public IActionResult AddToCart(string sku)
{
    var access = DelegatedShopper.Check(
        HttpContext.GetCheckpointResult(), CustomerRoot, "cart:write", out var customerId);
    if (access == ShopperAccess.Forbidden)
    {
        return StatusCode(StatusCodes.Status403Forbidden);
    }

    var cart = access == ShopperAccess.Shopper
        ? _carts.ForCustomer(customerId)
        : _carts.ForSession(HttpContext);
    cart.Add(sku);
    return RedirectToAction(nameof(Cart));
}

Both use the same helper. Pass your organization's DID, the same value as in TrustedDelegationRoots, and the scope the endpoint needs. It returns:

  • Forbidden for a verified grant that doesn't include the scope, whoever it comes from, so the endpoint refuses.
  • Shopper with the customer id for a verified grant that has the scope and comes from a user of the organization you passed.
  • Guest for everything else: no verified grant, a request the policy blocked, or a grant that has the scope but no customer id from that organization (a did:key, another host's or organization's DID, a user of another trusted root, extra segments, or no TrustedDelegationRoots configured). Serve the session's own cart, as for any visitor.
using System.Linq;
using Checkpoint.Core.Models;

public enum ShopperAccess
{
    Guest,
    Shopper,
    Forbidden,
}

public static class DelegatedShopper
{
    public static ShopperAccess Check(
        DetectionResult result, string trustedRoot, string requiredScope, out string customerId)
    {
        customerId = null;
        if (result == null ||
            result.VerificationMethod != VerificationMethod.KyaHttpDelegated ||
            result.ShouldBlock == true)
        {
            return ShopperAccess.Guest;
        }

        if (result.GrantedScopes == null || !result.GrantedScopes.Contains(requiredScope))
        {
            return ShopperAccess.Forbidden;
        }

        // CustomerId doesn't name its root, so user 42 from a second root would match too.
        var holder = result.DelegationHolder;
        if (holder?.CustomerId == null || holder.Subject != trustedRoot + ":users:" + holder.CustomerId)
        {
            return ShopperAccess.Guest;
        }

        customerId = holder.CustomerId;
        return ShopperAccess.Shopper;
    }
}
  • Name the scope per endpoint. Copying the cart page's call to a new endpoint without changing the scope gives that endpoint the cart page's access. Use the scope your consent screen asks the shopper to approve for that action.
  • Look the id up, don't trust it as a key blindly. Resolve it to your customer through your own lookup, and fall back to the guest experience when no customer matches.
  • Never read the credential yourself. The module has already verified the signature; decoding the Bearer token in your own code to make a decision skips that check.

Dashboard Reporting

Detection events flow to your Vouched KYA dashboard automatically: every AI agent and bot classified above the confidence threshold is reported fire-and-forget after the request completes. Pure human traffic is not reported. Same payload, same gating, and same dashboard experience across ASP.NET Core and .NET Framework.

Requires Checkpoint:ProjectId and Checkpoint:ApiKey to be set. Without them, detection and enforcement still run: only the dashboard hop is skipped.

Upgrading

Already running Vouched KYA and want to move to the latest release? The path is the same across runtimes: bump the package version, restore, redeploy. Some 1.x releases change behavior or remove public types, so read Breaking Changes Since 1.12.0 first.

The current stable release is 1.15.0.

# If you installed the metapackage (recommended)
dotnet add package KyaOs.Checkpoint --version 1.15.0

# If you installed the adapter package directly
dotnet add package Checkpoint.AspNetCore --version 1.15.0

After the bump: dotnet restore, rebuild, redeploy.

Binding Redirects on .NET Framework

A release can move a dependency such as System.Text.Json to a new assembly version, so check the redirects in Web.config on every upgrade. The package ships the set it needs in web.config.install.xdt (packages/Checkpoint.AspNet.<version>/content/ for packages.config, ~/.nuget/packages/checkpoint.aspnet/<version>/contentFiles/any/net462/ for PackageReference). NuGet inserts that set only when Web.config has no <runtime> block, so it never updates redirects you already have.

For each assembly in that file, set your bindingRedirect to its oldVersion and newVersion, or add the entry if it's missing. Visual Studio's Package Manager usually rewrites redirects during Update-Package (unless nuget.config sets bindingRedirects to skip); editing files by hand or with another tool doesn't. A redirect that doesn't match the DLL in bin/ fails every request with FileLoadException 0x80131040.

Should I Switch to the Metapackage?

If your csproj references Checkpoint.AspNetCore or Checkpoint.AspNet directly, both packages remain first-class and continue to ship at the same version cadence. You do not have to switch. Switching to KyaOs.Checkpoint is purely a discoverability and forward-compatibility convenience:

  • One install command works for any target framework (NuGet picks the adapter for you).
  • New runtimes added to the metapackage in the future are picked up automatically.
  • Namespaces and API are identical: using Checkpoint.AspNetCore.Extensions; and AddCheckpoint(...) continue to compile unchanged.

To switch, swap the package reference and rebuild: no source edits.

Breaking Change to Know About: Response Headers Renamed in 0.4.0

If you are upgrading from 0.3.x or earlier to 0.4.0 or later (which includes any move to a 1.x release), the custom response headers were renamed per RFC 6648 (deprecating the X- prefix) and the KYA-OS rebrand. The middleware behavior is identical: only the header names changed.

Pre-0.4.0 header0.4.0+ header
X-Checkpoint-DetectedKYA-Detected
X-Checkpoint-ConfidenceKYA-Confidence
X-Checkpoint-ClassKYA-Class
X-Checkpoint-AgentKYA-Agent
X-Checkpoint-VerificationKYA-Verification
X-Checkpoint-SessionKYA-Session
X-MCP-I-RequiredKYA-Auth-Required
X-MCP-I-AuthorizeKYA-Auth-Url
X-MCP-I-ProjectKYA-Project
X-AgentShield-ActionKYA-Action

If you have downstream code, log queries, alerts, WAF rules, or monitoring dashboards keyed off the old names, search-and-replace before deploying.

Breaking Changes Since 1.12.0

Crossing one of these versions can change what your site does. Move every Checkpoint package you reference to the same version. The changelog has the full list.

1.12.0

  • A request with no evidence either way reports IncompleteData, not Human. IsAgent stays false and composed rules still read it as human, but KYA-Class, DetectionClass and the class sent to the dashboard change. Check any code or report that reads them.
  • Protected-resource metadata names the authorization server the platform sends, and answers 404 when it sends none.
  • BuildDocument, BuildNativeDocument and BuildAccessGuide on ProtectedResourceMetadataBuilder take the platform's issuer as a parameter. Code that calls the old signatures no longer compiles.

1.13.0

  • FailOpen defaults to false. It was true through 1.12.x. A site with no FailOpen setting (Web.config Checkpoint:FailOpen) goes from fail-open to fail-closed on this upgrade. Choose the value on purpose before you upgrade:

    • false: an SDK error blocks the request with 503 and Retry-After: 3. A detection outage counts as an SDK error: on default options, a 32-bit app pool or a missing wasmtime.dll gets the same 503 on every request the module covers, until the host is fixed. A failed policy fetch with no cached policy answers every request it covers the same way, humans included. The cache is in memory, so an app pool recycle during an outage is enough. With a cached policy, the site keeps using it. In native mode the answer is 503 kyaos/policy-unavailable, and the cached policy is not used.
    • true: an SDK error lets the request through with KYA-Failopen, and a failed policy fetch falls back to the cached policy. With no cached policy, no policy rule applies, so a path you protect is not protected until the fetch recovers. A detection outage or SDK error is the exception: with OnAgentDetected set to Block, Redirect, Challenge or Instruct, every affected request the module covers gets that action unless OnDetectionUnavailable is Allow.

    IncludePaths limits which paths this can reach, because the module doesn't run on any other path. Those paths also get no detection.

  • A 200 from /api/internal/policies/{id} with no blockThreshold is treated as a failed fetch. Checkpoint's route always sends it. A proxy or test double standing in for that route has to send it too.

  • A presented credential that can't be verified returns 503 even with FailOpen = true. One the engine refuses keeps its 401 or 403.

  • A Cedar error on an enforcing project blocks the request, unless FailOpen is true.

  • WebMcpReadOnlyOptions.StatusListUrl, SignedStatusListVerifier and StatusList2021 are gone. The StatusListResolver constructor changed, and StatusListResult.ExpiresAtUnix is no longer nullable.

1.14.0

  • The embedded engine refuses MCP-I envelopes and the legacy Layer 2 delegation headers outside the native propagation lane with 403 kyaos/unsupported-carriage.
  • The SDK makes outbound HTTPS requests to feeds.kya-os.ai. With an egress allow-list, allow that host, set VendorKeyFeedUrl to a mirror, or set EnableVendorKeyFeed = false.
  • HttpProofClaim.ResourceDid is now HttpProofClaim.Scope, and some unused public types are gone from Checkpoint.Core.

1.15.0

  • Servers need outbound HTTPS to the trusted roots and to the did:web hosts of the agents and issuers they accept.
  • A status list must be hosted on its issuer's did:web host.
  • An engine block is answered as the engine renders it. BlockedResponse applies only to blocks the SDK decides.
  • The public resolution types are gone. Use ResolutionWarmer.CreateDefault.
  • VerificationMethod.McpIHandshake and AuthorizeInputProjection.IsUnverifiedOptimisticProtocolBlock are gone.

Verifying the Upgrade

After the redeploy, load a page as a person first. A FileLoadException (0x80131040) on the first request means a binding redirect is missing or doesn't match a DLL. See FileLoadException with HRESULT 0x80131040.

Then run these checks:

# 1. Confirm the new header set is present
curl -sI https://your-site.example.com/ -A "GPTBot/1.0" \
  | grep -i "^KYA-"

# Expect KYA-Detected, KYA-Confidence, KYA-Class, KYA-Agent at minimum.
# 2. Confirm classification is correct (a known bot should report bot, not ai_agent)
curl -sI https://your-site.example.com/ -A "Googlebot/2.1 (+http://www.google.com/bot.html)" \
  | grep -i "^KYA-Class"

# Expect: KYA-Class: Bot
# 3. Confirm dashboard rows show the correct detection class
# In your Checkpoint dashboard, filter the Detections table by
# source=middleware and detectionClass=bot. The Googlebot probe from
# step 2 should appear there within ~30 seconds.
# 4. If you enforce, confirm a protected path answers as it did before
curl -s -o /dev/null -D - -A "Claude-User/1.0" https://your-site.example.com/your-protected-path \
  | grep -iE "^HTTP|^KYA-Action"

# Expect the same status and KYA-Action as before the upgrade, unless a listed
# breaking change covers it.
# 5. In native mode, confirm the protected-resource metadata still answers
curl -s https://your-site.example.com/.well-known/oauth-protected-resource

# Expect a 200 with your resource. From 1.12.0, authorization_servers names
# the issuer the platform sends, so that value can change on this upgrade.

If every page answers 503, read the body and the response headers first.

  • The generic body with KYA-Detection: unavailable: detection can't run. The body is {"error":"Service temporarily unavailable","source":"checkpoint-middleware"} with Retry-After: 3, and it lasts until the host is fixed. On .NET Framework, check that wasmtime.dll is in bin/ and that the app pool is 64-bit. See a 32-bit host and a missing wasmtime.dll.
  • The generic body without that header: the site can't read its policy and FailOpen is false, or another SDK error failed closed. Check ProjectId, ApiKey, and outbound HTTPS from the server to BaseUrl.
  • kyaos/policy-unavailable: a native site that can't read its policy, or a native site with an enforcing policy where detection can't run. This 503 carries no KYA-Detection header, so use the trace log: Checkpoint startup phase=DetectorWarmup with outcome=Unavailable means detection. Check wasmtime.dll and the app pool first, then ProjectId, ApiKey, and outbound HTTPS.

If KYA-_ headers are absent but X-Checkpoint-_ are still present, the new package did not load: check that the restore ran and that the build output's Checkpoint.Core.dll reports the new version (AssemblyInformationalVersion matches 1.15.0).

If KYA-Class reports AiAgent for known crawlers like Googlebot, the embedded engine is stale: confirm Checkpoint.Core resolved to 1.15.0 in obj/project.assets.json, or in packages.config on a site that uses it. The dependabot lockstep groups in the demo repo can be used as a reference for keeping Checkpoint.* packages on a single version.

Troubleshooting

The errors below are specific to the .NET Framework adapter. Modern ASP.NET Core installs don't hit any of them.

Detection Unavailable on a 32-Bit Host

Symptom. The site starts, but detection can't run. From 1.13.0, on default options, every page the module covers answers 503 and no detections reach the dashboard. The trace log shows Checkpoint startup phase=DetectorWarmup with outcome=Unavailable. Most sites get the generic 503: it has Retry-After: 3 and KYA-Detection: unavailable, and each request logs Checkpoint module error (fail-closed, blocking). A native site with an enforcing policy answers kyaos/policy-unavailable instead, with neither that header nor that log line.

Cause. The app pool is running in 32-bit mode. Vouched KYA's WebAssembly runtime ships x64 only and has no 32-bit fallback, so detection can't run until the host is fixed. From 1.13.0, your settings decide what answers:

  • Default options: 503 on every page the module covers.
  • OnAgentDetected set to Block, Redirect, Challenge or Instruct: that action answers every page the module covers, people included, whatever FailOpen says, unless OnDetectionUnavailable is Allow.
  • FailOpen set to true with OnAgentDetected left on Log, or Checkpoint:OnDetectionUnavailable set to Allow: requests pass through with nothing enforced. A request that presents a credential doesn't pass through.

One exception: a native site with an enforcing policy and FailOpen set to false answers 503 kyaos/policy-unavailable whatever those settings say.

Fix.

  • Azure App Service. Upgrade the App Service Plan from Free (F1) or Shared (D1) to Basic (B1) or higher. In the portal, open Configuration โ†’ General Settings, set Platform to 64-bit, and restart the app.
  • IIS on Windows Server. In IIS Manager, open Application Pools โ†’ your pool โ†’ Advanced Settings and set Enable 32-Bit Applications to False.
  • Standalone host. Add <PlatformTarget>x64</PlatformTarget> to your csproj, or AnyCPU with <Prefer32Bit>false</Prefer32Bit>.

FileLoadException with HRESULT 0x80131040

Symptom. First request returns a 500, with an error like:

Could not load file or assembly 'Microsoft.Extensions.Logging.Abstractions'
or one of its dependencies. (0x80131040)

Cause. Classic ASP.NET needs binding redirects in Web.config for the transitive Microsoft.Extensions.* and System.Text.Json dependency graph. Checkpoint.AspNet 0.2.1+ ships an auto-generated redirect set inside the NuGet package, but it's applied differently depending on whether your project uses packages.config or PackageReference.

Fix for packages.config. On a first install into a Web.config with no <runtime> block, NuGet inserts the shipped set for you. If Web.config already had a <runtime> block, or after an upgrade, update the redirects from the shipped file as described in Binding redirects on .NET Framework:

packages/Checkpoint.AspNet.<version>/content/web.config.install.xdt

Fix for PackageReference. NuGet does not auto-transform content files for PackageReference projects. Copy the shipped redirect set into Web.config: open the XDT at the path below and merge its <runtime> block into yours.

~/.nuget/packages/checkpoint.aspnet/<version>/contentFiles/any/net462/web.config.install.xdt

AutoGenerateBindingRedirects doesn't fix this on its own for a web project: MSBuild writes the generated redirects to bin/<YourApp>.dll.config, which IIS doesn't read. Web.config must carry them.


wasmtime.dll Missing From bin

Symptom. The same as a 32-bit host: from 1.13.0, on default options, every page the module covers answers 503. The app pool is 64-bit, but wasmtime.dll isn't in bin/.

Cause. The NuGet consumer-side targets file that copies wasmtime.dll into your build output didn't fire. As of Checkpoint.Core 0.2.2+, the file ships at buildTransitive/net462/ and is imported automatically by both direct and transitive PackageReference consumers as well as packages.config consumers.

Fix.

  1. Install KyaOs.Checkpoint or Checkpoint.AspNet, not bare Checkpoint.Core. The targets file lives in the Framework adapter's transitive graph; a project that references only Checkpoint.Core directly won't pull it in.
  2. On a packages.config project, check the <Import> for Checkpoint.Core.targets in your .csproj. It names a package version, and it's what copies wasmtime.dll into bin/. It must name the version you installed.
  3. If the file is still missing, run msbuild /verbosity:detailed and search the log for Checkpoint.Core.targets. A missing import means NuGet's targets graph isn't being walked by your project type: email kya+support@vouched.id with a minimal repro.

Supported Versions

RuntimeVersionsPackage target
ASP.NET Core.NET 8, 9, 10net10.0 for .NET 10, net8.0 for .NET 8 and 9
.NET Framework4.6.2, 4.7, 4.7.1, 4.7.2, 4.8, 4.8.1net462

Since 1.11.9, the ASP.NET Core adapter ships two builds, net8.0 and net10.0. NuGet picks net10.0 for a .NET 10 app and net8.0 for a .NET 8 or 9 app, on its own. The adapter's tests run on .NET 8 and on .NET 10. .NET 6 and 7 are not supported, because the package has no build they can load.

Microsoft ends support for .NET 8 and .NET 9 on November 10, 2026. .NET 10 is the current long-term support release, supported until November 2028, so build new apps on it and plan upgrades for older ones.

.NET Framework requires a 64-bit host process (Windows Server IIS or Azure App Service B1+).

Next Steps