.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.
0 entries. Choose a section or search the full text.
Install
dotnet add package Backbay.ChioBackbay.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.
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());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.
using Backbay.Chio;
var project = await Computer.OpenAsync("quickstart");
Console.WriteLine(project.Describe());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 filesComputer.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.
var partner = await Computer.ConnectAsync("partner");
Console.WriteLine(partner.Describe());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.
foreach (var partner in await Computer.PartnersAsync()) Console.WriteLine(partner.Summary);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.
var fixer = await project.AgentAsync("fixer");
Console.WriteLine(fixer.Describe());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.
var reviewers = await project.AgentsAsync("review", count: 2, reads: reviews);
foreach (var reviewer in reviewers) Console.WriteLine(reviewer.Summary);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.
var fork = await project.ForkAsync();
Console.WriteLine(fork.Describe());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.
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());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 minutesAuthority.Reissue
Authority Reissue(Fork on)
Issues a grant with the same terms on another fork. The original grant is unchanged.
var next = await project.ForkAsync();
var again = authority.Reissue(next);
Console.WriteLine(again.Describe());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 minutesAgent.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.
var run = await fixer.RunAsync("fix issue #412", fork, authority);
Console.WriteLine(run.Describe());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.
await foreach (var e in run.EventsAsync()) Console.WriteLine(e.Summary);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.
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();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.
Console.WriteLine(await fork.DiffAsync());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.
var merged = await project.ApplyAsync(fork.Changes);
Console.WriteLine(merged.Describe());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.
var refusals = await project.History.ReceiptsAsync(outcome: Outcome.Deny);
Console.WriteLine(refusals[0].Describe());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.
var evidence = await project.History.ExportAsync("audit/");
Console.WriteLine(await evidence.VerifyAsync());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.
var rule = await project.Policy.AddAsync(new RuleSpec
{
Name = "tests-are-evidence",
Deny = new Match { Write = "/workspace/tests/**" },
});
Console.WriteLine(rule.Describe());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.
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());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.
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());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.
var rule = await pricing.RequireAsync(Approval.ByYou().When("quote.percent > 50"));
Console.WriteLine(rule.Describe());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.
await foreach (var call in pricing.EventsAsync()) Console.WriteLine(call.Summary);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.
Console.WriteLine(await pricing.UsageAsync(since: TimeSpan.FromHours(1)));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.
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());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.
await treaty.RevokeAsync();
await foreach (var e in run2.EventsAsync()) Console.WriteLine(e.Summary);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.
var reviews = await project.MailboxAsync("reviews", partner, treaty);
Console.WriteLine(reviews.Describe());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.
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);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.
var boot = Program.Parallel(Programs.Reproduce, Programs.Survey)
.Pipe(Programs.Patch)
.Pipe(Programs.Verify);
var fork = await project.ForkAsync(boot);
Console.WriteLine(fork.Describe());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.
Console.WriteLine(USD(2));
Console.WriteLine(USD(0.01m) * 100);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.
try
{
await project.ApplyAsync(fork.Changes);
}
catch (ApplyConflictException conflict)
{
Console.WriteLine(conflict.Message);
}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.
using Backbay.Chio;
builder.Services.AddChioProtection();
app.UseChioProtection();| Property | Value |
|---|---|
PackageId | Backbay. |
RootNamespace | Backbay.Chio |
TargetFramework | net8.0 |
Version | 0.1.0 |
Framework | Microsoft. |
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
dotnet add package Backbay.Chio.MiddlewareThe 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.
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.
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.
| Option | Default | Purpose |
|---|---|---|
SidecarUrl | CHIO_ 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. |
Identity | null, resolved to Chio (:80) | Custom caller extraction, typed Identity. |
RouteResolver | null, resolved to a function returning the raw path (:81) | Maps (method, path) to a route pattern. |
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.
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.
| Member | Endpoint | Returns |
|---|---|---|
Default | none | http://127.0.0.1:9090 |
EvaluateAsync(request, capabilityToken?) | POST /chio/evaluate | Task<Evaluate |
Verify | POST /chio/verify | Task<Verify |
Health | 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.
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.
| Type | Role |
|---|---|
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- |
ChioHttpRequest | Request posted to /chio/evaluate. |
Evaluate | Verdict, receipt, and evidence. |
Verify | 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. |
Chio | 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 Http 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.
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.