Decionis 0.3.0

dotnet add package Decionis --version 0.3.0
                    
NuGet\Install-Package Decionis -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="Decionis" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Decionis" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Decionis" />
                    
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 Decionis --version 0.3.0
                    
#r "nuget: Decionis, 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 Decionis@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=Decionis&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Decionis&version=0.3.0
                    
Install as a Cake Tool

Decionis .NET SDK

The Decionis .NET SDK is an execution interceptor for .NET 8 services. It captures execution intent, delegates policy judgment to Decionis, and runs the callback only when the signed decision allows or when shadow mode is explicit.

dotnet add package Decionis
var client = new DecionisClient(Environment.GetEnvironmentVariable("DECIONIS_API_KEY")!);

var result = await client.EnforceAsync(
    new DecisionRequest
    {
        TenantId = "trading_client_001",
        Actor = new() { ["id"] = "cfd_bot_7", ["type"] = "TRADING_BOT" },
        Action = new() { ["type"] = "OPEN_POSITION", ["instrument"] = "BTC-PERP" },
        PolicyRefs = ["cfd-risk-policy"]
    },
    async decision => await broker.OpenPositionAsync(order, decision.DecisionId));

Use EvaluateAsync for diagnostics. Use EnforceAsync(..., execute, shadow) for production execution boundaries so the callback runs only after Decionis returns ALLOW.

var shadow = await client.EnforceAsync(request, ExecuteTransferAsync, shadow: true);

The package includes ASP.NET Core middleware hooks and Ed25519 decision signature verification.

Execution authority: no grant, no execution

Decionis.Authority.ExecutionAuthorityClient runs a consequential action only after Decionis authorizes that exact intent and issues a single-use grant, and only after you claim that grant. A changed amount, account or target is a different intent, and its grant does not claim.

Step Call Route
Ask for a grant IssueGrantAsync(request) POST /v1/authority/grants
Check it without using it VerifyGrantAsync(token, request) GET /v1/authority/grants/{grantId}
Claim it once, bound to the exact parameters ClaimGrantAsync(token, request) POST /v1/authority/grants/{grantId}/claims
Record what happened RecordReceiptAsync(receipt) POST /v1/authority/grants/{grantId}/receipts
Follow a managed Presence escalation GetEscalationAsync, WaitForAuthorizationAsync GET /v1/authority/escalations/{escalationId}

EnforceAndExecuteAsync runs the whole sequence and fails closed:

using Decionis.Authority;

var authority = new ExecutionAuthorityClient(new ExecutionAuthorityClientOptions
{
    ApiKey = Environment.GetEnvironmentVariable("DECIONIS_API_KEY"),
    RequestSigner = ExecutorRequestSigner.FromPkcs8Pem("payments-executor-1",
        File.ReadAllText("/run/secrets/executor-key.pem")),
});

var request = AuthorityRequest.Bind(new AuthorityIntent
{
    TenantId = orgId,
    Actor = new AuthorityActor("ap-bot", "SERVICE"),
    Action = new AuthorityAction("create_vendor_payment", "erp:vendor:V-1009",
        new { amount = 18000.50m, currency = "EUR", vendor_id = "V-1009" }),
    DownstreamTarget = new DownstreamTarget("sap", "create_payment"),
});

var run = await authority.EnforceAndExecuteAsync(request, async (grant, ct) =>
{
    using var payment = new HttpRequestMessage(HttpMethod.Post, "https://ledger.example/v1/payments")
    {
        Content = new StringContent(CanonicalJson.JcsText(request.ActionParameters()), Encoding.UTF8, "application/json"),
    };
    await grant.SignAsync(payment, ct);
    return await http.SendAsync(payment, ct);
});

if (!run.Executed)
{
    // BLOCKED_BY_POLICY, NO_EXECUTION_GRANT, GRANT_NOT_REDEEMABLE or AUTHORITY_ERROR
    logger.LogWarning("Payment not released: {Reason}", run.BlockReason);
}
  • The action runs at most once, inside the claimed grant's lease. Send the request to the system of record through grant.SignAsync(...), so it can refuse anything this grant did not authorize (next section).
  • If the action throws, an INDETERMINATE receipt is recorded and the exception propagates. Classify a certain failure with new ExecutionOptions<T> { ClassifyFailure = _ => ReceiptOutcome.FAILED }.
  • A returned value is digested as RFC 8785 JSON into the receipt's effect evidence. A Protocol 1.1 grant records the receipt without that confirmation and notes PROTOCOL_1_1_EFFECT_EVIDENCE_REQUIRED.
  • SHADOW and PARALLEL requests observe only: they never authorize.
  • With escalation: new ManagedEscalationRequest(), an ESCALATE decision carries a managed-escalation locator. WaitForAuthorizationAsync polls it until Presence approves (GRANT_READY, with the decision whose grant you claim) or the intent expires.

Numbers are hashed as Decionis reads them. A long beyond 2^53 or a decimal with more precision than a double holds is refused with DECIONIS_INTENT_NUMBER_INEXACT, so the amount Decionis authorizes is the amount you send. Pass such values as strings. The intent hash, execution payload digest and grant id are held to the shared vectors in sdks/test/authority-vectors.json, the same ones @decionis/sdk and the Decionis server pass.

In ASP.NET Core, register the client with an IHttpClientFactory-managed HttpClient:

builder.Services.AddDecionisExecutionAuthority(options =>
{
    options.ApiKey = builder.Configuration["Decionis:ApiKey"];
    options.RequestSigner = ExecutorRequestSigner.FromPkcs8Pem(
        builder.Configuration["Decionis:ExecutorKeyId"]!, builder.Configuration["Decionis:ExecutorKeyPem"]!);
});

Sign the request to the system of record

A system of record that implements the Verifying Provider Profile at VP-2 effects a request only when all of these hold:

  • it is signed with an executor key the system of record issued or registered for you (RFC 9421 message signature with an RFC 9530 content digest);
  • the signature covers the grant id, the decision id, the intent hash, the idempotency key and the claim attestation;
  • the attestation verifies against https://api.decionis.com/.well-known/decionis-execution-grant-jwks.json and describes this request: this grant, this decision, this intent, and a body whose canonical JSON is the parameters the grant was claimed for;
  • the grant has not been presented before.

grant.SignAsync(request, ct) signs an HttpRequestMessage in place over the claimed grant. Forwarding ClaimAttestation on its own is not enough: a VP-2 provider refuses it with SIGNATURE_INVALID_OR_INCOMPLETE.

  1. Make an Ed25519 key and give the public half to whoever runs the system of record. They register it under a key id and tell you that id.

    openssl genpkey -algorithm ed25519 -out executor-key.pem
    openssl pkey -in executor-key.pem -pubout -out executor-key.pub.pem
    
  2. Set RequestSigner = ExecutorRequestSigner.FromPkcs8Pem(keyId, pem), or new ExecutorRequestSigner(keyId, seed) for the 32-byte private key you already hold. The key never leaves your service.

  3. In the action, send the parameters you authorized as the JSON body, with an absolute request URI, and call grant.SignAsync(...) before sending. Key order and whitespace are yours to choose; the system of record compares canonical JSON.

The signed request carries:

Header Value
idempotency-key the intent id
x-agent-safe-intent-hash the intent hash the grant was issued for
x-agent-safe-grant-id, x-agent-safe-decision-id the claimed grant and the decision that issued it
x-agent-safe-claim-attestation the claim attestation
content-digest sha-256=:<base64>: over the exact body bytes
signature-input, signature label agentsafe, alg="ed25519", your keyid

The signer replaces any header of the same name already on the request, and sends the body bytes it digested with the content headers you set. The query string is not covered, so keep anything that changes the effect out of it. For a request built some other way, take the same headers from grant.SignedHeaders(method, uri, body) and send body exactly. For a request outside EnforceAndExecuteAsync, call signer.SignAsync(request, new ExecutorDispatch(...)), or ExecutorDispatch.Read(...) for a read that changes nothing.

The signer is held to the profile's own vectors (sdks/test/verifying-provider), and its tests send signed requests over HTTP to the AgentSafe ASP.NET Core verifier (Decionis.VerifyingProvider), which accepts them once and refuses a replay or an altered body.

If you would rather not hold an executor key in each service, run the AgentSafe executor as a gateway in front of your systems of record, with DOWNSTREAM_CREDENTIAL_KIND=SIGNED_REQUEST: it claims the grant and signs every dispatch itself.

ASP.NET Core

app.UseDecionisGate(
    client,
    http => new DecisionRequest
    {
        Actor = new() { ["id"] = "orders-api", ["type"] = "SERVICE" },
        Action = new() { ["type"] = "HTTP_REQUEST", ["path"] = http.Request.Path.Value },
        PolicyRefs = ["api-execution-policy-v1"]
    });
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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 116 10/30/2026