Chio/Docs
LOGIN · JOIN

ReferenceSDKs

.NET SDK

Run agents on a computer from C#, and put ASP.NET Core services under kernel policy with a verified decision before each protected handler runs.

Install

bash
dotnet add package Backbay.Chio

Backbay.Chio carries the Computer API and targets net8.0. The ASP.NET Core middleware ships separately as Backbay.Chio.Middleware. Both import from the Backbay.Chio namespace.

Computer

Computer is the API the Quickstart uses. A computer is a versioned environment where authority, resources, and work live. You open one, fork it, grant an agent part of the fork, run work, read the signed receipts, and apply the changes you keep. Your kernel enforces every grant and signs every decision.

Calls that reach the kernel are async, end in Async, and accept a CancellationToken as their last parameter. Grant, Reissue, and Describe are synchronous. Event streams are IAsyncEnumerable<T>. using static Backbay.Chio.Money; brings USD into scope, as the examples assume.

This program is Quickstart part 1 in one file. It runs inside the computer that chio new quickstart creates.

Program.cscsharp
using Backbay.Chio;
using static Backbay.Chio.Money;

var project = await Computer.OpenAsync("quickstart");
var fixer = await project.AgentAsync("fixer");
var fork = await project.ForkAsync();
var authority = fork.Grant(new GrantSpec
{
    To = fixer,
    Resources =
    {
        ["/workspace"] = ["read", "write"],
        ["/models/default"] = ["invoke"],
    },
    Budget = USD(2),
    ExpiresIn = TimeSpan.FromMinutes(20),
});

var run = await fixer.RunAsync("fix issue #412", fork, authority);
await foreach (var e in run.EventsAsync()) Console.WriteLine(e.Summary);

var refusals = await project.History.ReceiptsAsync(outcome: Outcome.Deny);
Console.WriteLine(refusals[0].Describe());
Output
00:00  admitted   fixer runs "fix issue #412" on fork 51c07ae
00:03  read       /workspace/src/pricing.py
00:05  read       /workspace/tests/test_pricing.py
00:09  refused    read /secrets/.env: outside the grant
00:12  invoke     /models/default, USD 0.21
00:41  write      /workspace/src/pricing.py
00:56  test       pytest -k issue_412: passed
spent  USD 0.48 of 2.00

receipt    rcpt-01a8…9c04
request    fixer read /secrets/.env
verdict    deny: outside the grant
grant      cap-01a8…31f2
policy     sha256 3c7b…8010
signed     your kernel 4f1c…a2e9, signature verifies

The examples below continue from it: project is the quickstart computer, fixer is its agent, and fork and authority are the fork and grant above. The service examples use the pricing service from Quickstart part 2, and the partner examples use the partner card, treaty, and reviews mailbox from part 3.

Computer.OpenAsync

static Task<Computer> Computer.OpenAsync(string name, CancellationToken ct = default)

Opens a computer your operator identity owns and returns its handle. The handle reads the computer's current revision; nothing is copied.

csharp
using Backbay.Chio;

var project = await Computer.OpenAsync("quickstart");
Console.WriteLine(project.Describe());
Output
computer   quickstart
owner      did:chio:z6Mkf3…Qp9 (you)
revision   main @ 7c2e1d0
resources  /workspace         git, 14 files
           /models/default    model route, metered in USD
           /secrets           3 files

Computer.ConnectAsync

static Task<Partner> Computer.ConnectAsync(string name, CancellationToken ct = default)

Resolves a paired partner by the name you gave its card, and authenticates its kernel before returning.

csharp
var partner = await Computer.ConnectAsync("partner");
Console.WriteLine(partner.Describe());
Output
partner    partner, did:chio:z6MkpA7…c2T
operator   Halden, halden.example
kernel     9b07…d31c, pinned from its card
transport  iroh, authenticated at handshake

Computer.PartnersAsync

static Task<IReadOnlyList<Partner>> Computer.PartnersAsync(CancellationToken ct = default)

Lists every partner you have paired with, each with its pinned kernel key and the treaty in force.

csharp
foreach (var partner in await Computer.PartnersAsync()) Console.WriteLine(partner.Summary);
Output
partner    did:chio:z6MkpA7…c2T   Halden   kernel 9b07…d31c   treaty trt-01aa…4c19

Computer.AgentAsync

Task<Agent> AgentAsync(string name, CancellationToken ct = default)

Returns the agent with this name, issuing it an identity from your kernel the first time. An agent holds no authority until you grant it some.

csharp
var fixer = await project.AgentAsync("fixer");
Console.WriteLine(fixer.Describe());
Output
agent      fixer, did:chio:z6MkrT2…8wd
issued by  your kernel 4f1c…a2e9
grants     none

Computer.AgentsAsync

Task<IReadOnlyList<Agent>> AgentsAsync(string role, int count, Mailbox? reads = null, CancellationToken ct = default)

Starts several agents of one role, numbered from 1, optionally reading a shared mailbox.

csharp
var reviewers = await project.AgentsAsync("review", count: 2, reads: reviews);
foreach (var reviewer in reviewers) Console.WriteLine(reviewer.Summary);
Output
reviewer-1   did:chio:z6MkvR1…3mc   reads reviews
reviewer-2   did:chio:z6MkvR2…9tf   reads reviews

Computer.ForkAsync

Task<Fork> ForkAsync(Program? boot = null, CancellationToken ct = default)

Branches the computer's managed state at its current revision. Work on the fork stays there until you apply it. A fork does not reset authority, history, or spending.

csharp
var fork = await project.ForkAsync();
Console.WriteLine(fork.Describe());
Output
fork       51c07ae of quickstart, base main 7c2e1d0
writes     stay in the fork until you apply them

Fork.Grant

Authority Grant(GrantSpec spec)

Issues a capability token over part of the fork: resources and their rights, a budget charged at your door, and an expiry. GrantSpec.To takes an agent, a partner, or a DID; Delegation lets the holder delegate narrower grants onward, and Under issues a grant within a treaty. Issuance completes durably before the call returns.

csharp
var authority = fork.Grant(new GrantSpec
{
    To = fixer,
    Resources =
    {
        ["/workspace"] = ["read", "write"],
        ["/models/default"] = ["invoke"],
    },
    Budget = USD(2),
    ExpiresIn = TimeSpan.FromMinutes(20),
});
Console.WriteLine(authority.Describe());
Output
grant      cap-01a8…31f2, signed by your kernel
to         fixer, did:chio:z6MkrT2…8wd
on         quickstart, fork 51c07ae
allows     /workspace         read, write
           /models/default    invoke
budget     USD 2.00, charged at your door
expires    in 20 minutes

Authority.Reissue

Authority Reissue(Fork on)

Issues a grant with the same terms on another fork. The original grant is unchanged.

csharp
var next = await project.ForkAsync();
var again = authority.Reissue(next);
Console.WriteLine(again.Describe());
Output
grant      cap-01a8…5b3e, signed by your kernel
to         fixer, did:chio:z6MkrT2…8wd
on         quickstart, fork 7d2e914
allows     /workspace         read, write
           /models/default    invoke
budget     USD 2.00, charged at your door
expires    in 20 minutes

Agent.RunAsync

Task<Run> RunAsync(string task, Fork on, Authority authority, CancellationToken ct = default)

Sends a task to an agent under a grant. Every operation the agent attempts reaches your kernel before it reaches a resource.

csharp
var run = await fixer.RunAsync("fix issue #412", fork, authority);
Console.WriteLine(run.Describe());
Output
run        run-01a8…e7c0
agent      fixer, on fork 51c07ae
authority  cap-01a8…31f2

Run.EventsAsync

IAsyncEnumerable<RunEvent> EventsAsync(IEnumerable<Agent>? include = null, CancellationToken ct = default)

Streams the run's events in order: admissions, reads, writes, model calls, tests, and refusals. A refusal is an event with a receipt, not an exception, and the run continues.

csharp
await foreach (var e in run.EventsAsync()) Console.WriteLine(e.Summary);
Output
00:00  admitted   fixer runs "fix issue #412" on fork 51c07ae
00:03  read       /workspace/src/pricing.py
00:05  read       /workspace/tests/test_pricing.py
00:09  refused    read /secrets/.env: outside the grant
00:12  invoke     /models/default, USD 0.21
00:41  write      /workspace/src/pricing.py
00:56  test       pytest -k issue_412: passed
spent  USD 0.48 of 2.00

Partner.ExecAsync

Task<Run> ExecAsync(string task, Fork on, Authority authority, Mailbox? reportTo = null, CancellationToken ct = default)

Sends a task to a partner's agents under a grant issued within a treaty. The partner's kernel admits the work and runs it in its own process tree. Run.JoinTreeAsync waits for every agent the partner delegated to.

csharp
var run = await partner.ExecAsync("fix issue #414", fork, authority, reportTo: reviews);
await foreach (var e in run.EventsAsync()) Console.WriteLine(e.Summary);
await run.JoinTreeAsync();
Output
00:00  them  admitted   "fix issue #414" under trt-01aa…4c19
00:01  them  delegate   coordinator to fixer-1, fixer-2, tester, each narrower
00:31  them  refused    fixer-2 read /secrets/.env: outside the grant, at your door
01:47  them  sealed     fork c4e81b7 at 5b93d10
spent  USD 3.12 of 5.00 across 4 of their agents

Fork.DiffAsync

Task<string> DiffAsync(CancellationToken ct = default)

Returns the changes the fork holds against its base, as a unified diff.

csharp
Console.WriteLine(await fork.DiffAsync());
Output
src/pricing.py
@@ def discount(price, percent):
+    if not 0 <= percent <= 100:
+        raise ValueError("percent must be between 0 and 100")
     return price * (1 - percent / 100)

Computer.ApplyAsync

Task<Applied> ApplyAsync(ChangeSet changes, Treaty? treaty = null, CancellationToken ct = default)

Publishes the fork's exact changes if main still matches the fork's base, and returns the signed merge. Under a treaty, both kernels co-sign it.

csharp
var merged = await project.ApplyAsync(fork.Changes);
Console.WriteLine(merged.Describe());
Output
applied    fork 51c07ae onto quickstart main
revision   main 8d4b2e7, base 7c2e1d0
receipt    rcpt-01a8…d51e, signed by your kernel

History.ReceiptsAsync

Task<IReadOnlyList<Receipt>> ReceiptsAsync(Outcome? outcome = null, Agent? agent = null, TimeSpan? since = null, CancellationToken ct = default)

Reads signed receipts from the computer's history, filtered by outcome, agent, or time.

csharp
var refusals = await project.History.ReceiptsAsync(outcome: Outcome.Deny);
Console.WriteLine(refusals[0].Describe());
Output
receipt    rcpt-01a8…9c04
request    fixer read /secrets/.env
verdict    deny: outside the grant
grant      cap-01a8…31f2
policy     sha256 3c7b…8010
signed     your kernel 4f1c…a2e9, signature verifies

History.ExportAsync

Task<Evidence> ExportAsync(string directory, CancellationToken ct = default)

Writes the computer's history to a directory as an evidence package. Verification checks every signature against pinned kernel keys, offline.

csharp
var evidence = await project.History.ExportAsync("audit/");
Console.WriteLine(await evidence.VerifyAsync());
Output
receipts   9 signed decisions from your kernel
refusals   1: fixer read /secrets/.env
spend      USD 0.48 of 2.00
verified   offline, against your kernel key 4f1c…a2e9

Policy.AddAsync

Task<Rule> AddAsync(RuleSpec rule, CancellationToken ct = default)

Adds a named rule to the computer's policy. Policy narrows every grant on the computer, and no grant widens policy.

csharp
var rule = await project.Policy.AddAsync(new RuleSpec
{
    Name = "tests-are-evidence",
    Deny = new Match { Write = "/workspace/tests/**" },
});
Console.WriteLine(rule.Describe());
Output
rule       tests-are-evidence
denies     write /workspace/tests/**
policy     sha256 9a41…e6f2, narrows every grant on quickstart

Computer.ServiceAsync

Task<Service> ServiceAsync(string name, IReadOnlyDictionary<string, string> tools, CancellationToken ct = default)

Exposes project functions as named tools and publishes them over MCP and HTTP. Every caller presents a grant from your kernel.

csharp
var pricing = await project.ServiceAsync("pricing", new Dictionary<string, string>
{
    ["quote"] = "src/pricing.py:quote",
    ["refund"] = "src/pricing.py:refund",
});
var endpoint = await pricing.PublishAsync(Protocol.Mcp, Protocol.Http);
Console.WriteLine(endpoint.Describe());
Output
service    pricing, from quickstart @ main 2f60a91
tools      quote · refund
mcp        https://127.0.0.1:9440/pricing/mcp
http       https://127.0.0.1:9440/pricing/v1, OpenAPI at /openapi.json
admits     callers that hold a grant from your kernel

Service.Grant

Authority Grant(ServiceGrantSpec spec)

Grants a caller named tools on the service, with a call rate, a budget spent at each tool's price, and an expiry.

csharp
var access = pricing.Grant(new ServiceGrantSpec
{
    To = "did:chio:z6MkbY4…e1R",
    Tools = ["quote"],
    Limit = new Rate(100, TimeSpan.FromHours(1)),
    Budget = USD(2),
    ExpiresIn = TimeSpan.FromHours(24),
});
Console.WriteLine(access.Describe());
Output
grant      cap-01ab…77d1, signed by your kernel
to         did:chio:z6MkbY4…e1R
allows     pricing.quote
limits     100 calls per hour
budget     USD 2.00, USD 0.01 per call
expires    in 24 hours

Service.RequireAsync

Task<Rule> RequireAsync(Approval approval, CancellationToken ct = default)

Pauses each matching call until the approver signs for that exact request.

csharp
var rule = await pricing.RequireAsync(Approval.ByYou().When("quote.percent > 50"));
Console.WriteLine(rule.Describe());
Output
rule       quote.percent > 50 waits for did:chio:z6Mkf3…Qp9 (you)
binds      one signature to one request

Service.EventsAsync

IAsyncEnumerable<CallEvent> EventsAsync(CancellationToken ct = default)

Streams calls to the service as your kernel decides them.

csharp
await foreach (var call in pricing.EventsAsync()) Console.WriteLine(call.Summary);
Output
00:00  z6MkbY4  quote   sku-118 at 10%        allow     USD 0.01
00:04  z6MkbY4  refund  order-77              refused   not in the grant
00:05  z6MkQx8  quote   sku-118 at 10%        refused   no grant from your kernel
00:09  z6MkbY4  quote   sku-301 at 80%        pending   approval required
00:41  z6MkbY4  quote   sku-301 at 80%        allow     USD 0.01

Service.UsageAsync

Task<Usage> UsageAsync(TimeSpan since, CancellationToken ct = default)

Summarizes each caller's calls, refusals, approvals, and spend, computed from that caller's signed receipts.

csharp
Console.WriteLine(await pricing.UsageAsync(since: TimeSpan.FromHours(1)));
Output
caller             calls   refused   approvals   spent
z6MkbY4…e1R        100     2         1           USD 1.00
z6MkQx8…7Ha        0       1         0           USD 0.00

Computer.TreatyAsync

Task<Treaty> TreatyAsync(TreatyTerms terms, CancellationToken ct = default)

Agrees terms with a partner: the actions you cooperate on and what each requires. Both kernels sign, and each computes the intersection of the two governance ladders, so the stricter requirement wins.

csharp
var treaty = await project.TreatyAsync(new TreatyTerms
{
    Partner = partner,
    Actions = ["workspace.read", "workspace.write", "model.invoke", "merge"],
    ExpiresIn = TimeSpan.FromDays(7),
});
Console.WriteLine(treaty.Describe());
Output
treaty           trt-01aa…4c19, signed by both kernels
participants     you · partner (did:chio:z6MkpA7…c2T)
action           you       them      admitted as
workspace.read   allow     allow     either kernel admits under its own policy
workspace.write  allow     allow     either kernel admits under its own policy
model.invoke     allow     allow     charged at your door
merge            co-sign   co-sign   both kernels sign
expires          in 7 days

Treaty.RevokeAsync

Task RevokeAsync(CancellationToken ct = default)

Ends the treaty and every grant issued under it, including grants the partner's agents delegated to each other. The revocation reaches the partner's kernel over iroh, and its next call is refused.

csharp
await treaty.RevokeAsync();
await foreach (var e in run2.EventsAsync()) Console.WriteLine(e.Summary);
Output
00:00  them  admitted   "add banker's rounding as an option" under trt-01aa…4c19
00:04  you   revoked    trt-01aa…4c19, revocation root sent over iroh
00:05  them  refused    fixer-1 write /workspace/src/pricing.py: treaty revoked
00:05  them  stopped    their work closed, no effects after revocation

Computer.MailboxAsync

Task<Mailbox> MailboxAsync(string name, Partner partner, Treaty treaty, CancellationToken ct = default)

Opens a mailbox shared with a partner under a treaty. Each message is sent and claimed under its sender's or reader's own grant.

csharp
var reviews = await project.MailboxAsync("reviews", partner, treaty);
Console.WriteLine(reviews.Describe());
Output
mailbox    reviews, under trt-01aa…4c19
senders    partner's agents, each under its own grant
readers    your agents that hold a reads grant

Runs.FirstPassingAsync

static Task<Run> Runs.FirstPassingAsync(IEnumerable<Run> runs, string check, CancellationToken ct = default)

Waits on several runs and returns the first whose sealed fork passes the check. The other runs are cancelled, and their receipts stay in history.

csharp
var ours = await fixer.RunAsync("fix issue #414", forkA, grantA);
var theirs = await partner.ExecAsync("fix issue #414", forkB, grantB);
var winner = await Runs.FirstPassingAsync([ours, theirs], check: "pytest");
Console.WriteLine(winner.Describe());
await project.ApplyAsync(winner.Changes);
Output
winner     run-01ab…0c7d, partner on fork c4e81b7
check      pytest: 14 passed
others     cancelled, receipts kept in history

Program.Parallel and Pipe

static Program Program.Parallel(params Program[] programs); Program Pipe(Program next)

Composes boot programs, the pinned descriptors your project exports. Parallel runs programs at once; Pipe runs them in sequence. This is the canonical form the Python and Rust operators normalize to. A fork started with a boot program runs it under the fork's grant.

csharp
var boot = Program.Parallel(Programs.Reproduce, Programs.Survey)
    .Pipe(Programs.Patch)
    .Pipe(Programs.Verify);
var fork = await project.ForkAsync(boot);
Console.WriteLine(fork.Describe());
Output
boot       (reproduce & survey) | patch | verify
fork       a3d90f1 of quickstart, base main 2f60a91

Money.USD

static Money Money.USD(decimal amount)

Constructs an exact dollar amount for budgets and prices. Money wraps a decimal, so cents are exact.

csharp
Console.WriteLine(USD(2));
Console.WriteLine(USD(0.01m) * 100);
Output
USD 2.00
USD 1.00

ApplyConflictException

class ApplyConflictException : ChioException { string Base; string Current; }

Apply refuses to publish when main moved after the fork was taken. Nothing changes; fork again from the new main and reapply.

csharp
try
{
    await project.ApplyAsync(fork.Changes);
}
catch (ApplyConflictException conflict)
{
    Console.WriteLine(conflict.Message);
}
Output
main moved from 7c2e1d0 to 9e03a4b; fork 51c07ae was not applied

How the Computer reaches the kernel

Each grant is a chio.capability.v1 token signed by your kernel. Each operation in a run is a mediated call, and each event carries the id of a chio.receipt.v1 receipt in the store that history reads. Policy rules compile into the HushSpec policy every grant on the computer is checked against. The Computer and the Kernel maps each Computer object onto its kernel mechanism. The low-level clients below speak to the same kernel and the same records without the Computer layer.

Protect an ASP.NET Core service

Chio’s .NET middleware connects your existing HTTP application to the Rust kernel. The sidecar evaluates requests entering a protected branch; the middleware requires an allowed, verified decision before invoking the handler. Your application keeps its routes and business logic.

Prepare a .NET 8 SDK, an ASP.NET Core project, and a running Chio sidecar configured for the service. Start with the package and middleware registration. For explicit evaluation outside the middleware, use ChioSidecarClient.

Registration · namespace Backbay.Chiocsharp
using Backbay.Chio;

builder.Services.AddChioProtection();
app.UseChioProtection();
PropertyValue
PackageId
Backbay.Chio.Middleware
RootNamespace
Backbay.Chio
TargetFramework
net8.0
Version
0.1.0
FrameworkReference
Microsoft.AspNetCore.App

The package id and the namespace differ: types are imported from Backbay.Chio, the RootNamespace the package sets. The package declares that one framework reference and no other dependency, so the middleware pulls in no third-party runtime code.

Install the middleware

From your web projectbash
dotnet add package Backbay.Chio.Middleware

The middleware needs a running Chio sidecar. ChioSidecarClient.DefaultSidecarUrl is http://127.0.0.1:9090 (ChioSidecarClient.cs:32), and the constructor prefers the CHIO_SIDECAR_URL environment variable over it when no base URL is passed (ChioSidecarClient.cs:40).

Quickstart

AddChioProtection (ChioMiddlewareExtensions.cs:269) registers the services and UseChioProtection (:287) inserts the middleware.

sdks/dotnet/ChioMiddleware/README.md:29-39csharp
using Backbay.Chio;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddChioProtection();

var app = builder.Build();
app.UseChioProtection();

app.MapGet("/pets", () => new { pets = Array.Empty<object>() });

app.Run();

The middleware fails closed. A sidecar it cannot reach produces a 502 and a structured JSON body (ChioMiddlewareExtensions.cs:169, :176, :186). A denied evaluation returns the verdict's HttpStatus when it is positive and 403 otherwise (:198). An allowed request reaches the next component only after the embedded receipt passes the authorization check and verification at the same sidecar. The middleware then sets X-Chio-Receipt-Id to the receipt id (:210). The header identifies the decision made before the handler runs; it does not establish that the handler completed successfully.

The middleware evaluates seven verbs: GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS (ChioMiddlewareExtensions.cs:69). Any other method short-circuits with a 405 and the error code chio_evaluation_failed before the sidecar is contacted (:97).

Excluding a route

UseChioProtection governs requests that enter its branch. To keep a health endpoint independent of the sidecar, exclude it with a predicate passed to UseWhen. The predicate below excludes exactly /healthz, case-insensitively; MapGet ordering alone does not create this exclusion.

PricingApp.cscsharp
    internal static WebApplication Create(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddChioProtection();

        var app = builder.Build();
        app.MapGet("/healthz", PricingEndpoints.Health);
        app.UseWhen(
            context => RequiresChioProtection(context.Request.Path),
            branch => branch.UseChioProtection());
        app.MapPricingEndpoints();
        return app;
    }

    internal static bool RequiresChioProtection(PathString path) =>
        !path.Equals("/healthz", StringComparison.OrdinalIgnoreCase);

In your pricing service, the outside buyer's POST /v1/quote returns 200 with the receipt id on X-Chio-Receipt-Id; its POST /v1/refund, which the grant does not cover, returns 403 and a ChioErrorResponse body carrying chio_access_denied, the deny reason, the receipt id, and the fixed suggestion provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter (ChioMiddlewareExtensions.cs:204).

Configuration

AddChioProtection accepts an Action<ChioMiddlewareOptions> configure callback. The options type is at ChioMiddlewareExtensions.cs:22-52.

OptionDefaultPurpose
SidecarUrl
CHIO_SIDECAR_URL env, else http://127.0.0.1:9090
Base URL of the sidecar kernel.
TimeoutSeconds
5
Timeout for each sidecar HTTP request. Evaluation and receipt verification make separate requests.
OnSidecarError
deny
Reserved no-op. Sidecar errors always fail closed.
IdentityExtractor
null, resolved to ChioIdentityExtractor.DefaultExtract (:80)
Custom caller extraction, typed IdentityExtractorDelegate.
RouteResolver
null, resolved to a function returning the raw path (:81)
Maps (method, path) to a route pattern.
csharp
builder.Services.AddChioProtection(opts =>
{
    opts.SidecarUrl = "http://127.0.0.1:9090";
    opts.TimeoutSeconds = 5;
    opts.RouteResolver = (method, path) =>
        path.StartsWith("/pets/") ? "/pets/{petId}" : path;
});

The middleware reads a capability token from the X-Chio-Capability header (ChioMiddlewareExtensions.cs:246) or the chio_capability query parameter (:255) and forwards it to the sidecar.

Identity extraction

ChioIdentityExtractor.DefaultExtract fingerprints the presented credential: a bearer token, then an API key, then the first cookie, with anonymous fallback. It stores a SHA-256 hash in the caller record and leaves Verified false. It does not authenticate those credentials.

Use IdentityExtractorDelegate to connect the caller record to your application’s authentication layer. This example reads authentication state and subject from the same identity established by your configured authentication scheme. Incoming user or tenant headers need their own validation before they can supply identity claims.

Registration · after configuring application authenticationcsharp
using System.Security.Claims;
using Backbay.Chio;

builder.Services.AddChioProtection(opts =>
{
    opts.IdentityExtractor = request =>
    {
        var caller = ChioIdentityExtractor.DefaultExtract(request);
        var identity = request.HttpContext.User.Identity as ClaimsIdentity;
        var subject = identity?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (identity?.IsAuthenticated == true &&
            !string.IsNullOrEmpty(subject))
        {
            caller.Subject = subject;
            caller.Verified = true;
        }
        return caller;
    };
});

// Authentication handlers must already be configured in your application.
app.UseAuthentication();
app.UseChioProtection();

Run application authentication before Chio middleware. The caller record describes the application’s identity check; capability and policy checks still determine whether the request is authorized. Credential hashes in that record do not mean all secrets are removed from the evaluation: capability tokens are forwarded separately, and query parameters are included in the request. Prefer X-Chio-Capability when supplying a capability.

ChioSidecarClient

The middleware wraps ChioSidecarClient (ChioSidecarClient.cs:30), which also runs standalone. It serializes with JsonNamingPolicy.SnakeCaseLower and omits null fields (ChioSidecarClient.cs:46-50). The middleware sends body hashes when a positive ContentLength is available (ChioMiddlewareExtensions.cs:111-125). It implements IDisposable; Dispose (:159) disposes the inner HttpClient, so construct it with using.

MemberEndpointReturns
DefaultSidecarUrl
none
http://127.0.0.1:9090
EvaluateAsync(request, capabilityToken?)
POST /chio/evaluate
Task<EvaluateResponse>
VerifyReceiptAsync(receipt)
POST /chio/verify
Task<VerifyReceiptResponse>
HealthCheckAsync()
GET /chio/health
Task<bool>
Dispose()
none
void

The optional second argument to EvaluateAsync is the capability token. When it is neither null nor whitespace the client adds an X-Chio-Capability header to the evaluation request (ChioSidecarClient.cs:65-68).

EvaluateAsync does not trust an allow-shaped response on its face. When the verdict or the embedded receipt looks like an allow, it re-verifies through /chio/verify and requires VerifyReceiptResponse.Authorizes to hold before returning; otherwise it throws ChioSidecarException with code chio_invalid_receipt (ChioSidecarClient.cs:97-114). VerifyReceiptAsync swallows a non-success status and a transport failure alike and returns a default VerifyReceiptResponse, whose booleans are all false, so a failed verification reads as a refusal.

csharp
using Backbay.Chio;

using var client = new ChioSidecarClient("http://127.0.0.1:9090");

var request = new ChioHttpRequest
{
    RequestId = Guid.NewGuid().ToString(),
    Method = "GET",
    RoutePattern = "/pets",
    Path = "/pets",
    Caller = CallerIdentity.CreateAnonymous(),
    Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
};

var result = await client.EvaluateAsync(request);
if (result.Verdict.IsAllowed() && result.Receipt.IsAuthorized())
{
    Console.WriteLine(result.Receipt.Id);
}

Types

The type graph lives in ChioTypes.cs under the Backbay.Chio namespace. Serialized request, response, and receipt properties declare their snake_case wire names with JsonPropertyName.

TypeRole
AuthMethod
Kind of credential presented, plus its hash. Credential validation is separate. Factories: Anonymous, Bearer, ApiKey.
CallerIdentity
Subject, auth method, verified flag, tenant, agent id. CreateAnonymous() builds the fallback.
Verdict
Kernel verdict, with IsAllowed() and IsDenied(). The string property VerdictType serializes as verdict.
GuardEvidence
Per-guard evaluation evidence.
HttpReceipt
Signed receipt. IsAuthorized() at line 205 encodes the mediated-authorization predicate.
ChioHttpRequest
Request posted to /chio/evaluate.
EvaluateResponse
Verdict, receipt, and evidence.
VerifyReceiptResponse
Authority result from /chio/verify. Authorizes() at line 313 is the strict gate.
ChioPassthrough
Reserved degraded-state marker with Mode, Error, and Message. Its own doc comment says the middleware always fails closed.
ChioErrorResponse
The structured error body: error, message, and the null-omitted receipt_id and suggestion.
ChioErrorCodes
The five error-code constants, listed under Errors.
ChioContextKeys
One constant, Passthrough, whose value is the string ChioPassthrough. It names an HttpContext.Items key.

Nothing under sdks/dotnet/ChioMiddleware/src writes ChioContextKeys.Passthrough or constructs a ChioPassthrough: the two are declared and unused, which is what the marker's doc comment means by reserved. A consumer reading HttpContext.Items for that key finds nothing, because the middleware denies instead of passing through.

The authorization predicates

HttpReceipt.IsAuthorized() (ChioTypes.cs:205-210) checks fields without verifying the signature. It requires ReceiptKind mediated_decision, BoundaryClass prevent, a null ObservationOutcome, TrustLevel mediated, and an allow verdict. VerifyReceiptResponse.Authorizes() (:313-327) layers on top: the verify response must carry Ok, Authorized, SignatureValid, SignerTrusted, ReceiptIdValid, and ParameterHashValid, the same three classification strings, a Result of allow, the receipt's own predicate, a lower-hex 64-character id and content hash, and a non-empty signature. The Receipt format page carries the full field set.

Errors

A standalone ChioSidecarClient distinguishes a non-success evaluation HTTP response (chio_evaluation_failed, with its StatusCode) from a transport failure (chio_sidecar_unreachable). Middleware maps both cases to a 502 response with chio_sidecar_unreachable.

ChioSidecarException carries a Code and an optional StatusCode. The middleware maps it to the response: an invalid receipt becomes a 502 with chio_invalid_receipt, any other sidecar failure a 502 with chio_sidecar_unreachable, and a denied verdict the verdict status with chio_access_denied.

csharp
try
{
    var result = await client.EvaluateAsync(request);
}
catch (ChioSidecarException ex)
{
    Console.WriteLine($"{ex.Code}: {ex.Message}");
}

ChioErrorCodes (ChioTypes.cs:384-391) holds the codes: chio_access_denied, chio_sidecar_unreachable, chio_evaluation_failed, chio_invalid_receipt, and chio_timeout. Each is part of the client or middleware error vocabulary. The timeout constant is declared but unused: evaluation timeouts become chio_sidecar_unreachable. See Schemas and errors taxonomy.

Source and verification

Package metadata comes from the .NET sources. Middleware registration and dispatch are in ChioMiddlewareExtensions.cs; HTTP calls are in ChioSidecarClient.cs. ChioTypes.cs defines the wire types, and ChioIdentityExtractor.cs defines caller extraction.