ThinkAgentKit.AspNetCore.Conformance 0.3.0

dotnet add package ThinkAgentKit.AspNetCore.Conformance --version 0.3.0
                    
NuGet\Install-Package ThinkAgentKit.AspNetCore.Conformance -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ThinkAgentKit.AspNetCore.Conformance" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ThinkAgentKit.AspNetCore.Conformance" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="ThinkAgentKit.AspNetCore.Conformance" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ThinkAgentKit.AspNetCore.Conformance --version 0.3.0
                    
#r "nuget: ThinkAgentKit.AspNetCore.Conformance, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ThinkAgentKit.AspNetCore.Conformance@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ThinkAgentKit.AspNetCore.Conformance&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=ThinkAgentKit.AspNetCore.Conformance&version=0.3.0
                    
Install as a Cake Tool

ThinkAgentKit for ASP.NET Core

ThinkAgentKit.AspNetCore is the host half of a ThinkAgentKit deployment. It owns the security boundary a browser and a Cloudflare Worker meet at, and it owns the tool capabilities a remote agent may invoke.

What it provides

  • Connection grants. A short-lived ES256 token bound to exactly one route, one purpose, and one single-use identifier, published for verification through a rotating JWKS document.
  • Authority leases. An opaque, revocable, hashed-at-rest credential the Worker stores in connection state. The browser never sees one.
  • A tool kernel. Typed C# descriptors that are the single source of truth for names, schemas, permissions, approval metadata, timeouts, and idempotency, plus a canonical manifest and its hash.

Registration

builder.Services.AddThinkAgentKit(builder.Configuration);

// The four seams a host application must supply.
builder.Services.AddSingleton<IThinkAuthorizationService, MyAuthorizationService>();
builder.Services.AddSingleton<IThinkSigningKeyProvider, MySigningKeys>();
builder.Services.AddSingleton<IGrantRedemptionStore, MyRedemptionStore>();
builder.Services.AddSingleton<IAuthorityLeaseStore, MyLeaseStore>();
builder.Services.AddThinkToolCatalog(MyTools.CreateCatalog());

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapThinkAgentKit();

ThinkAgentKit:LeaseHashSalt and ThinkAgentKit:WorkloadCredential are required secrets with no source-code default; startup fails without them.

Threat boundaries

Boundary What is trusted What fails closed
Browser to host The authenticated session, an allowed origin, and a double-submit antiforgery token Any cross-site request, any route or project the current user cannot access, any lifetime beyond policy
Browser to Worker Nothing. The Worker verifies the grant against published keys Signature, time, purpose, route, protocol version, origin, or a replayed identifier
Worker to host The Worker's own workload credential, plus a lease the host resolves server-side An unknown, revoked, expired, or route-mismatched lease; a membership that has since been removed; a manifest hash that is not the active one
Approval The verified identity recorded when the action was proposed Anyone other than the requester, and any authority that is no longer current at execution time

Claims and invocation envelopes are provenance, never authorization: every tool call re-asks the application whether this principal may still act on this project. Error responses are bounded stable codes, and no token, lease, cookie, or handler detail is ever logged or forwarded to the model.

Model messages on tool failures

An IThinkToolHandler.ExecuteAsync handler returns ToolInvocationResponse. The original Failure(code, detail = null) signature remains available for compiled handlers. Opt in to guidance written for the model with the explicit three-argument overload; pass detail: null when no diagnostic detail is needed:

return ToolInvocationResponse.Failure(
    ThinkErrorCodes.ToolInputInvalid,
    detail: "Host-only diagnostic information",
    modelMessage: "9 channels requested, the limit is 8 — split them across several requests.");

A Model message is optional plain text written for the model and must contain no internals or secrets. detail remains host-only and never reaches the model. The kit maps U+2028/U+2029 to \n, strips other control characters plus U+202A–U+202E, U+2066–U+2069 and U+FEFF, and replaces lone surrogates with U+FFFD. Emoji ZWJ sequences are preserved. It trims the text and caps it at 500 UTF-16 code units including …, trimming trailing whitespace after the cut and preserving surrogate pairs. An empty result is omitted. The Worker repeats this sanitation on the untrusted wire. The model receives { ok: false, code, message }: the code's fixed sentence, then one space and the Model message when present. Transport failures and Worker-created refusals have only the fixed sentence.

Protocol version stays 1: modelMessage is an additive optional invocation response field; existing consumers ignore it and responses without it keep their existing behavior. The protocol's exact-version checks remain unchanged.

Artifacts

A tool that produces a figure, a result table or a download saves it as an Artifact on the host and returns only a reference. The bytes never travel in tool output - the model sees the reference, small, and a browser fetches the bytes from the artifact endpoint under its own session.

// Optional seam: where Artifacts are kept. Without one, saving throws and the
// endpoint knows no ids. The reference adapters live in ThinkAgentKit.AspNetCore.Testing;
// registered this way their size bound is ThinkArtifactOptions.MaxArtifactBytes.
builder.Services.AddDiskThinkArtifactStore(
    Path.Combine(builder.Environment.ContentRootPath, "artifacts"));

// Optional policy; these are the defaults.
builder.Services.Configure<ThinkArtifactOptions>(options =>
{
    options.MaxArtifactBytes = 10 * 1024 * 1024;
    // image/png, image/svg+xml, text/csv, application/json, text/plain
    options.AllowedContentTypes.Add("image/jpeg");
});

In a handler:

var chart = await context.SaveArtifactAsync(svgBytes, "image/svg+xml", "Pump rates", cancellationToken);
var table = await context.SaveArtifactAsync(csvStream, "text/csv", "Pump rates", cancellationToken);
return ToolInvocationResponse.Success(new { chart, table });
// or, for one file, the reference itself: Success(chart), with
// OutputSchema = ThinkSchema.ArtifactRef(). The Worker hands it to the model
// and the browser as { ok: true, id, contentType, title, bytes }.

and in the descriptor, so the output contract (and the generated zod module) says so:

OutputSchema = ThinkSchema.Object(
    new Dictionary<string, ThinkSchema>
    {
        ["chart"] = ThinkSchema.ArtifactRef("The chart, as an SVG image."),
        ["table"] = ThinkSchema.ArtifactRef()
    },
    ["chart", "table"])

The public surface:

API What it is
ThinkToolExecutionContext.SaveArtifactAsync(ReadOnlyMemory<byte> or Stream content, string contentType, string title, CancellationToken) Saves under the call's tenant, project and chat route, taken from the verified Lease - never from tool input or the envelope's conversation id - and answers a ThinkArtifactRef.
ThinkArtifactRef(string Id, string ContentType, string Title, long Bytes) What an output carries. Serializes as { id, contentType, title, bytes }.
IThinkArtifactStore SaveAsync(record, stream), FindAsync(id), OpenReadAsync(id). Stores keep records and never decide access; ThinkArtifactStoreConformance asserts the duties the endpoint relies on (exact tenant/project/route round-trip, ordinal ids, malformed ids answer nothing, exactly record.Bytes bytes or a refusal).
ThinkArtifactOptions MaxArtifactBytes (default 10 MiB, at most 100 MiB) and AllowedContentTypes. HTML, XHTML, every XML type but SVG (text/xml, application/xml, text/xsl, any +xml), script types and multipart/x-mixed-replace are refused at startup.
ThinkSchema.ArtifactRef(description) A closed object of the four fields, for output schemas. It flows into the manifest and the generated zod module like any object schema.
InMemoryThinkArtifactStore, DiskThinkArtifactStore Reference adapters (.Testing), registered with AddInMemoryThinkArtifactStore() / AddDiskThinkArtifactStore(root). Both refuse an Artifact over their MaxArtifactBytes and keep the newest MaxArtifactsPerChat (50) per chat, evicting that chat's oldest, also under concurrent saves. Registered through those extensions the size bound is ThinkArtifactOptions.MaxArtifactBytes; constructed by hand it is 10 MiB unless given ThinkArtifactStoreLimits.From(options), and its refusal says so. Both verify the content length against record.Bytes. The disk store keeps {id}.bin beside {id}.json under one private root (never serve it as static files), for one process: a save that fails or is cancelled leaves nothing, and each save sweeps bytes no metadata names, so a crash cannot grow it unboundedly. Its retention reads every metadata file on each save. Neither bounds the number of chats; use them for development and tests.

Saving refuses, with ThinkArtifactException, a content type outside the allow-list, content over the limit and an empty title; titles are stripped of control and bidi characters and capped at 200 characters. Ids carry 192 random bits, base64url-encoded.

The artifact endpoint and its security model

MapThinkAgentKit() serves GET /api/think/artifacts/{id}. The request names the chat the Artifact belongs to with the grant request's route fields: ?projectId=…&agentClass=…&agentInstance=…&path=…. Checks run in this order, and every refusal before the lookup is decided without looking at the id:

  1. Cross-site protection: the double-submit antiforgery token is required (a navigation, an <img> or a form cannot send it), a present Origin must be allowed, and Sec-Fetch-Site must not be cross-site - 403 grant_cross_site.
  2. The session - 401 grant_unauthenticated.
  3. The route is canonical and of a supported class - 400 grant_invalid_route.
  4. Live project authority, IThinkAuthorizationService.AuthorizeRouteAsync for the named project and route - 403 with its code (grant_forbidden). A member removed a moment ago is refused here with a still-valid cookie.
  5. The id resolves, and the Artifact's stored tenant equals the reader's and its project and route equal the request's. An unknown id and another tenant's, project's or chat's Artifact all answer one identical 404 not_found.

The parity is of status and body. It is not timing parity: a lookup that finds a record does a little more work than one that does not. Ids carry 192 random bits, so the endpoint is not a useful oracle, but do not read "identical refusals" as constant time.

A served Artifact carries Content-Type from the store, X-Content-Type-Options: nosniff, Content-Disposition: attachment with a sanitized ASCII filename, Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; sandbox, and Cache-Control: no-store - not merely private, because authority is decided live on every read and a copy in the browser's HTTP cache would outlive a revocation or a sign-out. Even an SVG that carries script is therefore inert as a document; the Angular tak-artifact shows images only through an <img>, where SVG never runs script. A file a person downloads is saved as an application/octet-stream blob; opening that saved file later from disk happens outside the kit and outside the page's origin, as with any other download.

Running the tests

Install Node 22 and run npm ci at the repository root first. The full host test suite compiles generated TypeScript and verifies Zod and model JSON schemas using the installed typescript, zod and ai packages. The NuGet publishing workflow installs these prerequisites before running the same tests.

dotnet test                                       # every host test
dotnet test --filter FullyQualifiedName~Security  # one suite

Generating the Worker contract

npm run generate:host-tools   # writes the manifest, Worker module and browser companion
npm run check:host-tools      # regenerates twice and fails on any difference

An unsupported C# schema shape fails generation rather than emitting an untyped wrapper. A descriptor's output schema, when it declares one, is emitted beside its input schema as outputSchema (artifact references included); the Worker runtime does not use it. Regenerate after upgrading the generator. Schema descriptions, including property and nested descriptions, are emitted as Zod .describe(...) metadata and retained in model JSON schemas. Tool and schema descriptions use escaped JSON string literals in TypeScript.

Tool presentation metadata

Set optional init-only properties on a descriptor alongside its model-facing Description, for example DisplayName = "Fetch project facts" and IconKey = "search". A display name must be nonblank, single-line and at most 120 UTF-16 units, without control characters, bidi embedding/isolate controls or U+FEFF. An icon key must match [a-z][a-z0-9._-]* and contain 1–64 characters. Catalog construction rejects invalid values with the tool/property identified in the error. Null omits the field; descriptors setting neither property retain their exact prior canonical manifest bytes and hash.

The manifest and generated host-tools.ts carry those values. The generator also emits an import-free host-tools.display.ts exporting hostToolDisplay for every host tool and HOST_TOOL_MANIFEST_HASH. Consumer CLI commands can route the companion with --display-out ui/src/app/generated/host-tools.display.ts; pass the same option to host-tools check. Keep the .NET generator and Worker CLI versions matched and commit all generated artifacts. Bind the companion map to Angular [toolDisplay], and map icon keys to your icon set using [toolIconTemplate]. The kit provides a gear fallback, not an icon library.

Handlers may return an own top-level string summary (declare it in an output schema when that schema is closed). Successful default cards render it as text, even without display metadata. Angular prefers the bounded output and falls back to raw output only when that value is nullish. It reads an 8,192 UTF-16-unit prefix, strips bidi embedding/isolate controls and U+FEFF, folds whitespace to one line and caps at 200 units including the ellipsis without splitting surrogate pairs. Do not put HTML in the field expecting it to render.

These properties leave existing constructors and accessors intact. As members of a record they participate in value equality, hashing and printing; consumers comparing or snapshotting descriptors should account for populated display values. Metadata changes the manifest hash when present, so regenerate/deploy host and Worker together. Protocol version remains 1.

Package installation

ThinkAgentKit connects an ASP.NET Core host to the ThinkAgentKit Cloudflare Worker and Angular libraries.

  • ThinkAgentKit.AspNetCore supplies host integration, security contracts and tool descriptors.
  • ThinkAgentKit.AspNetCore.Testing supplies reference adapters and test helpers without a test framework dependency.
  • ThinkAgentKit.AspNetCore.Conformance supplies xUnit suites for host implementations.
  • ThinkAgentKit.ToolManifest is a local .NET tool that generates the Worker contract from a compiled host catalog.

Install the runtime with dotnet add package ThinkAgentKit.AspNetCore --version 0.3.0. Install the generator with dotnet tool install ThinkAgentKit.ToolManifest --local --version 0.3.0 after creating a tool manifest with dotnet new tool-manifest.

Run the generator with dotnet tool run think-agent-kit-manifest -- <output-directory> <host-assembly> <Namespace.Type.CatalogMember>. Use matching versions of the generator, host package and Worker package.

Requires .NET 10. Source and integration documentation: https://github.com/Polycrest-Labs/ThinkAgentKit

Licensed under MIT. Copyright (c) 2026 Polycrest Labs.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 0 10/9/2026
0.2.2 0 10/9/2026
0.2.1 0 10/9/2026
0.2.0 25 10/9/2026
0.1.0 100 9/10/2026