# .NET SDK reference

Package-level reference for installation, public interfaces, observation boundaries and delivery behaviour.

## Package and basic usage

Sigillum Finans .NET SDK 0.11.0


.NET 8, 9 and 10 use this normal `net8.0` NuGet package and the same shared
Java/.NET/Python observation contract. Official OpenTelemetry dependencies
are mandatory package dependencies. Distribution is commercially licensed;
public registry publication is separate from this package build.



## Normal ASP.NET Core setup


Reference `Sigillum.Finans.Sdk.AspNetCore` from the supplied local NuGet source;
it references the core `Sigillum.Finans.Sdk` normally. The one .NET delivery ZIP
contains the two Sigillum packages. NuGet resolves their declared third-party
dependencies through the institution's normal package mirror. Worker/console projects use
only the core package and do not require the ASP.NET Core shared runtime. Put the
service-specific ready configuration at
`AppContext.BaseDirectory/.sigillum/config.json`, or provide an explicit
absolute configuration path. Add the normal package registration:

```csharp
using Sigillum.Finans.Sdk;

builder.Services.AddSigillum();
builder.Services.AddHttpClient();
// After building the application, before handlers that consume request bodies:
app.UseSigillum();
```

The registration connects a ready handler to clients created by
`IHttpClientFactory`; the middleware observes inbound ASP.NET Core requests.
It does not require a second customer-owned helper project or hidden HTTP
profile. A directly constructed `HttpClient` uses the supplied handler:

```csharp
var client = new HttpClient(new SigillumHttpHandler(observer)
{
    InnerHandler = new HttpClientHandler()
});
```

Unread inbound requests are complete and empty only when native HTTP/1.0 or
HTTP/1.1 framing proves no body: no Transfer-Encoding, and no Content-Length or
one valid zero length. Finalization also requires no observed bytes/chunks or
capture loss. The HTTP method alone never proves emptiness. Duplicate,
conflicting or invalid framing, HTTP/2, HTTP/3 and unknown protocol metadata
remain incomplete until native consumption proves completion. The SDK never
reads, drains or rewinds a request to make this determination.

For a worker or batch, start the SDK once and use a configured named entry:

```csharp
using var observer = new SigillumClient(absoluteConfigPath);
using var operation = observer.BeginOperation("configured-execution-name");
// Existing native work continues here.
```

Configuration, trust preparation and delivery run on the background worker.
The business path does not wait for readiness, network, disk, a receipt or a
flush. `Dispose` stops new capture without waiting for pending delivery.
Calls made before configuration becomes ready are reported as observation
loss; they are never presented as captured. `Status` distinguishes readiness,
pending memory, verified durable custody, loss and retry attempts.



## Configuration


The shared `sigillum.sdk-config/2` file contains these fields:

| Field | Meaning |
|---|---|
| `formatVersion` | `sigillum.sdk-config/2` |
| `institutionId`, `environmentId`, `serviceId` | Actual service scope; nonzero UUIDs |
| `sourceToken` | This service's own source credential |
| `transferEndpoint` | Exact HTTPS `/api/transfer` URL |
| `rootCertificatePem` | Installation public CA; hostname checks remain enabled |
| `custodyTrust` | `keyId`, `spkiBase64`, `sha256Pin` for the pinned P-256 custody receipt key |
| `scopeTrust` | Pinned P-256 public key verifying product-issued execution scopes |
| `contextKeyBase64` | 32-byte institution/environment propagation key |
| `entryPoints` | Actual starts: `applicationId`, `entryPointId`, `kind`, `operation`, `path`, signed `scopeToken` |
| `limits` | `maxQueuedBytes`, `maxQueuedRecords`, `requestTimeoutMs`, `retryDelayMs`, `chunkBytes` |
| `localTransfer` | Optional product Agent `endpoint` (`http://127.0.0.1:<port>`) and a distinct 32–256 character printable ASCII `token` without spaces |

With `localTransfer`, the SDK's observer worker sends bounded SFRAW001 raw
captures to the installed Sigillum Agent at `/v1/captures`. Attachment hashes,
observation envelopes, SFREC001 packaging, encryption, durable retry and
upstream TLS run in that separate product process. Private official OTEL
lifecycle and export still run on the SDK observer worker. Neither native
capture nor application startup waits for the Agent. An unavailable local
Agent retains the same bounded RAM retry behavior; it never activates a remote
delivery fallback in the institution process. Failed local handoffs back off
the endpoint with bounded exponential delay and small jitter, starting at the
configured retry delay and capped at 30 seconds (or a larger configured delay).
A valid local receipt resets the failure streak; direct-mode retries retain
their existing configured fixed delay.

The exact HMAC-authenticated `localDurable` receipt releases pending SDK RAM
only after the Agent's durable local handoff. `Status.LocalDurableRecords`
counts these handoffs separately from `Status.DurableRecords`, which continues
to count remote custody in direct mode. A local receipt is not final central
evidence acceptance. Signed profiles come through the same Agent's
`/v1/profiles` route and retain the existing scope signature and revision
checks. Configuration without `localTransfer` retains direct HTTPS delivery.

An execution entry uses `kind: execution`, an exact `operation` and no path.
An HTTP entry uses `kind: http`, an exact HTTP method and exact path.
An already connected shared service can use `entryPoints: []`: its actual
application and execution arrive in the signed observer carrier. Without a
valid carrier or matching entry the SDK does not guess an application.

Each entry carries a product-signed, immutable application/service scope. The
SDK verifies its P-256 signature, pinned key, institution/environment, actual
entry identity and bounded service set on its worker before enabling roots.
There is no unsigned or version-1 configuration fallback. Every root admission
freezes that scope token and its UTC start time. Existing executions keep their
original scope through all children and retries.

The background worker refreshes source profiles every 30 seconds from the
same-origin authenticated `/api/transfer/profiles` endpoint. TLS, timeout and
the 1 MiB response bound remain enforced. Only a fully verified monotonic
registry revision replaces the active entry snapshot. The refresh is independent
of delivery retry; business methods never perform configuration network I/O.
Known source revocation stops new roots without rewriting accepted work.

A connected service excluded from an incoming execution's signed service set
does not create spans, HTTP records, AI facts or reference records. It preserves
the incoming observer span and can forward the authenticated carrier to the
next connected service. Its returned scope has `IsObserved == false`; dispose
it normally so that the previous ambient context is restored. This applies to
`BindContext` and both synchronous and asynchronous HTTP calls. Inclusion in
another application's scope does not expand the current application's scope.

Default limits are 64 MiB, 1024 records/active scopes, a 5000 ms worker request
timeout, 1000 ms retry delay and 65536-byte stream chunks. Active private OTEL
spans share the finite memory/slot budget. Pending encoded delivery and
encoding workspace are included in conservative memory reservations. Queue
capacity may be lowered, not raised above the shared 64 MiB/1024 ceiling.



## Public types and methods


`SigillumClient` is the service-wide observer. `SdkOptions`, `SdkLimits`,
`SdkEntryPoint` and `ReceiptTrust` are the typed configuration alternatives.
`SdkOperation` is a disposable execution/step scope; its `Context` is a
detached public `SdkContext` snapshot containing actual execution and span IDs
and the frozen `ScopeToken`. Reference arrays returned by `Current` or
`Context` are detached from active and queued SDK records.

| Method/property | Use |
|---|---|
| `BeginOperation(name)` | Begin a configured worker/batch execution; no login/session is required |
| `BeginHttpOperation(method, path, incomingContext)` | Ready inbound HTTP boundary used by the middleware |
| `BeginStep(name)` | Create a real child of the current observer scope |
| `SdkOperation.Fail(exception)` | Record safe error type/cancellation; never read a virtual exception message |
| `Current`, `ExportContext()`, `BindContext(carrier)` | Inspect/export/bind the signed independent observer context |
| `SetReferences(SdkReferences)` | Add optional session/person/external references without merging executions |
| `TryRecord(family, kind, rawJson, attachments, schema, links)` | Generic actual fact with an extensible raw JSON object |
| `Model`, `Classification`, `Retrieval`, `Tool`, `Embedding` | Named AI fact helpers accepting raw JSON |
| `Inference`, `Evaluation`, `Policy`, `Explanation` | Prediction/control/source-produced explanation helpers |
| `Source`, `Custom` | Source comparison/custom fact plus optional exact content |
| `ObserveAiAsync(name, rawInput, nativeCall, token)` | Invoke one native call once; capture supplied input and returned raw output; preserve the native exception/cancellation |
| `TrySetOtelAttribute`, `TryAddOtelEvent`, `TryAddOtelLink` | Bounded enrichment of the SDK's real private OTEL span |
| `Status` | Ready, queued records/bytes, remote custody records, local durable handoffs, lost records, failed attempts and last observer error |

The generic families are `lifecycle`, `ai`, `http`, `otel`, `context`, `source`
and `custom`. Their contents retain actual field names, absent fields,
explicit nulls, arbitrary precision numeric lexemes and extension fields.
The returned capture boolean acknowledges bounded RAM admission only. Raw JSON
object/depth/duplicate validation runs on the observer worker. Invalid admitted
data is never delivered; its reservation is released and an explicit encoding
loss report is queued under the original frozen execution scope.
The SDK does not call arbitrary object getters, enumerate lazy application
objects or infer a model's hidden reasoning. Use already materialized UTF-8
JSON and arrays, rather than application object serialization.

```csharp
observer.Classification("""
    {"intent":"actual-source-intent","score":0.9876543210987654321}
    """u8.ToArray());

observer.SetReferences(new SdkReferences(
    SessionRef: existingSessionReference,
    PersonRef: existingPersonReference,
    External: [new("institution.trace", existingTraceReference)]));
```

`SdkAttachment(Id, MediaType, Purpose, Bytes)` carries exact binary/text bytes.
`SdkLink(TraceId, SpanId, Relation)` carries actual explicit links.
Content bytes are copied under the admission budget, then hash/length/exact-byte
references are generated by the worker. Standard capture allows JSON objects
up to 8 MiB minus envelope overhead, depth 32, up to 128 attachments of at most
16 MiB each, subject to the total 32 MiB packet and finite queue budget.
Oversize, malformed or saturated observations fail capture and report loss;
they do not throw into native work or silently become complete records.

OTEL attribute helpers accept known scalar or homogeneous primitive-array
JSON values. Events and links accept JSON objects of those attributes. These
use OTEL's actual integer/double value types; use the generic raw JSON fact
surface when exact arbitrary-precision number lexemes are required. Each
helper reserves memory before retaining values, releases replacement credit,
and reports capacity rejection. Limits are 1024 attributes, 1024 events,
1024 links and 4096 primitive array elements, always subject to the shared
byte budget. No independent source string truncation is applied. Up to 64
external references are accepted. No raw private `Activity` is exposed.



## Automatic capture and delivery


Supported HTTP boundaries capture method, target, headers, response status,
actual consumed request/response bytes, stream chunks/offsets/terminal state,
safe error/cancellation type and actual ancestry. Bodies are not read ahead.
Both `HttpClient.Send` and `SendAsync` use the same capture contract and invoke
the native transport once. Native synchronous content serialization and stream
access remain synchronous. Successful stream repositioning or resizing is
marked partial; a reposition after an earlier terminal appends an explicit
loss fact rather than rewriting history.
Header capture allows 4096 name/value pairs and 1 MiB total UTF-8 names/values;
over-capacity output explicitly states `complete: false` and reports loss.
Header snapshots and JSON workspace reserve the same finite ledger before
copying. A wrapped content object's header-copy credit remains held until
the application disposes that content. If credit is unavailable, the native
content remains unchanged and the observation loss stays explicit.
Disposal before EOF, capacity loss and incomplete consumption remain partial.
The native value, exception, cancellation token and stream ownership remain
with the application. Existing `traceparent`/`tracestate` headers and native
OTEL providers, samplers/exporters and `Activity.Current` are preserved.

Each SDK instance owns an always-on private OTEL provider/source with its own
configuration, independent of institution OTEL environment/sampler settings.
All official `Activity` start/stop and listener callbacks run on the observer
worker. The business path creates only bounded product execution/span context
and immutable facts. A slow global institution listener can stall observation
delivery but cannot enter the business call through this instrumentation.

Actual private spans include resource, instrumentation scope, attributes,
events, links, status and actual dropped/observation limit counts. Their
real OTEL IDs are explicitly exposed as `nativeContext`, and the product's
causal IDs as `observerContext`. A labelled observer-context link connects
them; the IDs are never falsely presented as identical. Explicit source
facts retain `institutionProvided` origin; automatic HTTP/lifecycle facts use
`sdkAutomatic`, and real OTEL spans use `nativeOtel`.

The independent `x-sigillum-context` v2 carrier is scope checked and MAC verified.
Its frozen scope token is independently signature verified; possessing the
institution's propagation MAC key does not authorize a different service set.
It always includes an optional-reference object. If references would exceed
the 16 KiB wire limit, only their propagation is omitted; all original local
facts are retained, the signed causal core continues, and
`referencePropagationPartial` is reported. Active references are not changed.
It is observation metadata, never business authorization. SDK-free components
are not inspected; if they pass the carrier, connected endpoints keep the
actual relationship. Parallel calls remain separate children. Missing context
is never reconstructed by time, content, person or session equality.

The worker batches only already-ready records from the same execution and
frozen profile, at most 16 records and 8 MiB without waiting for a threshold.
A larger valid single record travels alone within the 32 MiB transport bound.
It retries identical packet bytes and delivery ID. A failed packet stays in the same byte/slot reservation
and waits its backoff in a fair bounded retry rotation; it does not prevent
other executions from reaching durable custody. No retry creates another copy
of the pending packet or releases its credit before a verified receipt.
Only an actual root-span record from its own root
service repeats the immutable root admission. Child-span batches keep an
empty admission list, including children in that same process; a shared-service
continuation never invents a foreign admission. The
memory copy is released only after the exact scoped record/package/delivery
bindings and pinned P-256 custody receipt verify. This receipt proves durable
product custody, not final central publication. RAM before that receipt is
not durable and can be lost on process failure. The product-owned transfer
worker performs durable encrypted storage and subsequent retries.
The observer delivery transport uses at most one concurrent IPv4 and one
IPv6 connection attempt, cancelling and disposing the losing connection.
The original hostname, TLS server name and strict installation CA checks
remain intact. This configuration never changes the application's HTTP client.

