Invarix.Gate.Evidence 1.0.0-rc.3

Suggested Alternatives

Invarix.Gate.Evidence 1.0.0-rc.4

Additional Details

Superseded by 1.0.0-rc.4 of the Invarix.Gate packages. With another package lifting Microsoft.Extensions.AI.Abstractions (ModelContextProtocol, the OpenAI provider), Invarix.Gate.Extensions.AI 1.0.0-rc.1 to rc.3 can fail on the first request; rc.4 requires Microsoft.Extensions.AI 10.4.0. rc.3 and later also fix approval requests that could carry credentials unmasked (advisory INVARIX-2026-001, https://invarix.dk/security) and an Agent Framework defect that ended every run once an agent's total tool calls passed the per-run ceiling. rc.4 also reads dynamic SQL, always-true WHERE clauses and file tools with an operation argument.

This is a prerelease version of Invarix.Gate.Evidence.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Invarix.Gate.Evidence --version 1.0.0-rc.3
                    
NuGet\Install-Package Invarix.Gate.Evidence -Version 1.0.0-rc.3
                    
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="Invarix.Gate.Evidence" Version="1.0.0-rc.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Invarix.Gate.Evidence" Version="1.0.0-rc.3" />
                    
Directory.Packages.props
<PackageReference Include="Invarix.Gate.Evidence" />
                    
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 Invarix.Gate.Evidence --version 1.0.0-rc.3
                    
#r "nuget: Invarix.Gate.Evidence, 1.0.0-rc.3"
                    
#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 Invarix.Gate.Evidence@1.0.0-rc.3
                    
#: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=Invarix.Gate.Evidence&version=1.0.0-rc.3&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Invarix.Gate.Evidence&version=1.0.0-rc.3&prerelease
                    
Install as a Cake Tool

Invarix.Gate.Evidence

Writes Invarix.Gate tool-call verdicts into an Invarix.Guard.Evidence decision log.

Gate decides what an agent is allowed to do. Evidence keeps an append-only record of AI decisions in CloudEvents 1.0 form. This package is the bridge: every verdict Gate renders is mapped to a decision record and queued for writing, without putting evidence I/O on the tool-call path. A record the queue evicts or the store fails to write is counted, not retried (see "Latency and loss").

Needs a Gate license token, in observe mode as well as enforce mode, whether the bridge is registered or built by hand. The token is free for a company under the Community revenue threshold and paid above it; see License below.

Setup

services.AddInvarixGate(gate => gate
    .Mode(GateMode.Enforce)
    .UseDefaultPack());

services.AddInvarixGuardEvidence(evidence =>
{
    evidence.AISystemId    = "support-agent";
    evidence.AISystemVersion = "3.1.0";
    evidence.ModelId       = "gpt-4o";
    evidence.ModelVersion  = "2024-11-20";
})
.UseJsonlStore("/var/evidence/decisions.jsonl");

services.AddInvarixGateEvidence();

Order matters. AddInvarixGateEvidence throws at startup if either of the other two calls is missing, and names the one to add. It also throws InvalidLicenseException without a valid Gate license token. It does not see a token passed to Mode(GateMode.Enforce, token): set LicenseToken in its options, or set the INVARIX_GATE_LICENSE environment variable, which Gate's own license check also reads. It does not check for a store: without .UseJsonlStore(...) or another store or sink, AddInvarixGuardEvidence keeps its no-op sink and every record is discarded.

A host that builds the bridge itself and attaches it with GateEngine.AddObserver, instead of calling AddInvarixGateEvidence, gets the same license check from the GateEvidenceBridge constructor. It reads the same LicenseToken option, falls back to the same environment variable, and throws the same InvalidLicenseException with the same message.

The AI-system and model identity come from AddInvarixGuardEvidence and are not repeated in the bridge's options. One declaration, validated once, so the two cannot drift.

modelId on a Gate record is the agent's model, not Gate. Gate runs no model; its decision path is pattern matching. The field names the model whose tool call was judged.

The mapping

Gate Decision record
every evaluated call one record of type dk.invarix.gate.toolcall.v1
a resolved approval a second record of type dk.invarix.gate.approval.v1
tool name subject
ToolCallRequest.CanonicalArgumentsJson inputHashSha256, SHA-256 of its UTF-8 bytes with no NFC step (Evidence's FORMAT-SPEC.md 2.4 applies one)
no output (Gate decides before the tool runs) outputHashSha256 = SHA-256 of the empty string
each RuleFinding a detectorsFired entry: name = rule id, redactedPreview = redacted match, masked for credentials again
finding action Allow / Warn / Escalate,Deny,Terminate detector verdict pass / warn / block
nothing fired outcome: allowed
a warning outcome: allowedWithWarnings
observe mode, would have blocked outcome: allowedWithWarnings
enforce mode, denied or terminated outcome: blocked
escalated, awaiting a human outcome: blocked, oversight manualReview
approval granted outcome: overridden, oversight override
approval refused outcome: blocked, oversight reverse
operator stop-run outcome: blocked, oversight stopButton
nobody decided, on_approval_timeout: allow or warn (the call runs) outcome: allowedWithWarnings, oversight none, no operator
nobody decided, on_approval_timeout: block (the call does not run) outcome: blocked, oversight none, no operator

Only the exact argument digest is recorded. Gate's loop hash is deliberately lossy: it drops volatile arguments and folds names to lowercase, so it does not commit to what actually ran. It never appears in evidence.

Escalations are resolved in GateEngine.ResolveApprovalAsync. The overload taking the call, which the shipped adapters use, notifies approval observers, this bridge among them, and hands them the broker's ApprovalDecision, so the resolution and the operator behind it are recorded with no extra wiring. A host that resolves approvals through its own machinery calls GateEvidenceBridge.OnApprovalResolved(resolved, call, decision) with the verdict it got back and the broker's decision. Pass the decision whenever an operator answered: without one, the record says that nobody decided. The overload of ResolveApprovalAsync without the call does not hand the decision back, so a host that wants the resolution recorded uses the overload that takes the call.

When the engine gets no decision from the broker, because the approval timed out, the broker failed, or the engine resolving it has no broker, it applies limits.on_approval_timeout and the approval record says that nobody decided: no operatorId, oversight action none, and an outcome that says whether the call ran. allow and warn run the call and are recorded as allowedWithWarnings. block, the default, does not run it and is recorded as blocked. The engine's gate.approval finding is recorded as a detector result, pass under allow and warn under warn, which tells those two apart. A broker answer that is not approve, deny or stop-run counts as a broker failure and is recorded the same way. The policy file and GateBuilder accept only allow, warn and block for on_approval_timeout. A GateLimits built by hand is not checked: a resolution it leaves escalated is recorded as blocked with none, because no adapter runs it, and one that ends the run is recorded as blocked with stopButton and no operator.

A decision the broker returns is recorded as that decision, whether or not a person gave it. ConsoleApprovalBroker returns a deny under its own operator id (by default console: followed by the OS user name) when no interactive console is attached, when console input has ended or cannot be read, and when a request is cancelled before it reaches the prompt, as happens when the approval timeout passes while another escalation holds the console. Those are recorded as blocked with reverse under that id, and on_approval_timeout does not apply to them. WebhookApprovalBroker in Invarix.Gate.AspNetCore throws when it cannot deliver a request, so its failures are recorded as nobody deciding.

The Evidence format has no value for "the approval timed out and the policy decided", so this is the existing combination that claims nothing that did not happen. overridden with override would name an override nobody made, reverse a refusal nobody gave, and manualReview a call that is still held. allowedWithWarnings is the outcome a call that ran although a rule wanted it stopped already gets in observe mode. Under allow the flag behind it is the escalating rule's block detector result, not a warning, which reads the format's "at least one detector warned" loosely; allowed would say every detector passed. The oversight object is written with none rather than left out. Invarix.Guard.Evidence omits it when no operator interacted, so its presence could be read as an interaction; on an approval record it is there so that a query finds every unanswered escalation by that one value.

The format's outcome and oversight values are closed sets, so a dedicated value would be a change to Invarix.Guard.Evidence's FORMAT-SPEC.md. Its one open field is the CloudEvents type, which may be any reverse-DNS name, so a separate dk.invarix.gate.* type for unanswered escalations would need no spec change. The bridge keeps one approval type so that every resolution is read from one place and the unanswered ones are found by oversight none. The record does not say which of the three causes it was; Gate logs the cause through the host's logger when it resolves the approval.

The tool-call record for the same call is written while the call is held, so it stays blocked with manualReview whatever happens next; the approval record after it says what did. A host that cancels the approval wait gets no resolution from the engine, and the held tool-call record is then the only one. An engine in enforce mode with no broker never escalates its own verdicts: it denies an escalating call as it evaluates it, so the shipped adapters write a blocked tool-call record and no approval record.

The two logs carry different things

Gate's own verdict log (docs/VERDICT-LOG.md in the Gate package) records the same argument digest, as argsSha256, alongside the mode, the applied and observed actions, each finding's detail text, the ledger counters (calls, tokens, and an advisory cost estimate), and the policy pack version. Apart from the digest, none of that reaches the evidence store.

DecisionData is sealed, has a fixed field set, no extension slot, and a wire format frozen by golden-vector tests. There is no honest place to put those values, so the bridge does not put them anywhere: nothing is encoded into a detector name, packed into a redacted preview, or appended to the subject. A record carrying a value in a field that does not mean it is not evidence, it is a decoding puzzle whose answer nobody wrote down.

The same limit costs the record the agent id, the run id, and the call id. If you need evidence searchable by run, set CorrelateByRunId = true and the run id becomes the record's correlationId instead of the ambient trace id.

An optional extensions object on DecisionData is a plausible future change to Evidence. It is not something to work around now.

Latency and loss

IGateVerdictObserver.OnVerdict is synchronous and runs while the agent waits. Mapping happens there, because it is pure CPU work and has to see the verdict as rendered. The write is queued and drained by a background task, so no tool call waits on a disk.

The queue is bounded (QueueCapacity, default 4096). When it fills, the oldest record is evicted: blocking would put evidence latency back on the tool-call path, and dropping the newest would keep the start of an incident and lose the part where it went wrong. Every eviction, failed write, and record abandoned at shutdown increments DroppedRecordCount, and the first one logs a warning. Non-zero means the log is incomplete for that process, except that a count taken at a drain timeout is an upper bound: the writer may still land some of those records.

A write failure is counted and swallowed, so a store that starts failing does not break a running agent. A store that cannot be opened is different: the JSONL store opens its file the first time the evidence sink is resolved, which is when the host starts or when the Gate engine is first resolved, whichever comes first, and a failure there throws.

Disposal stops intake and drains the queue, bounded by DrainTimeout (default 5 seconds). Dispose the service provider before the process exits: records still queued when a process ends without disposal are lost, and DroppedRecordCount does not count them.

Tamper evidence

Records written through this bridge are tamper-evident only once the host seals them with its own keys. Invarix.Guard.Evidence ships Merkle batch commitments and Ed25519 signing as a library you call: the host reads a batch of records, signs the batch's Merkle root with a key it controls, and keeps the signed commitment. Nothing seals automatically, and AddInvarixGuardEvidence registers no sealing step. Unsealed, a JSONL evidence file is plain text that anyone with write access can edit.

Even with sealing, the claim is bounded. A signed batch proves that its records were not altered after the batch was sealed; a record can still be edited between being written and being sealed. A signature cannot prove that a compromised process wrote honest records in the first place: something that has already taken over the host can sign whatever it likes with the key that host holds. Sealing raises the cost of quiet retroactive edits. It is not proof of what happened.

License

Elastic License 2.0, with the Invarix commercial terms at the end of the packaged LICENSE file. Evidence export needs a Gate license token (SKU gate-professional) whichever mode Gate runs in, free for a company under the Community revenue threshold and paid above it. The package checks it when the bridge is registered or built: AddInvarixGateEvidence at registration, and the GateEvidenceBridge constructor when you build the bridge yourself. To get a Community token, email sales@invarix.dk with the company name. A paid license includes a window of updates, after which the last version you received keeps working, with no kill switch and no phone home. No support of any kind is included. Current terms and prices: invarix.dk/gate.

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 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.