---
title: "Caller-credential propagation (LatticeCredentialContext) - Lattice Public API Reference"
url: "https://nsta1.github.io/Orleans.Lattice/docs/lattice/api/caller-credential-propagation-latticecredentialcontext.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/lattice/api.md?plain=1#L958-L1086"
package: "Orleans.Lattice"
version: "9.9.0"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
bundle: "https://nsta1.github.io/Orleans.Lattice/docs/lattice/llms-full.txt"
---
# Caller-credential propagation (`LatticeCredentialContext`)

Part of [Lattice Public API Reference](../api.md).

`LatticeCredentialContext` is a transport-only ambient seam that carries
an opaque caller credential from the client edge down to the silo on the
Orleans `RequestContext`, following the same marker idiom as
`LatticeOriginContext` and `LatticeIdempotencyContext`. It is the channel
the (separately registered) Membership layer resolves into a subject. The
core library never interprets it - its resilient scan and range-delete
helpers only re-assert the caller's credential around each reconnect - so
an unset credential adds no cost and changes no read/write semantics.

Stamp a credential ergonomically at the boundary of a logical operation:

```csharp verify
using (LatticeCredentialContext.Use("edge-token", scheme: "Bearer"))
{
    await tree.SetAsync("k", new byte[] { 1 }, cancellationToken);
}
```

`Use` accepts the opaque token plus three optional hints an authenticator
can consult without re-parsing the token: a `scheme` / issuer hint, a
pre-resolved `principalId`, and a small `metadata` bag. `With(LatticeCredential?)`
takes the full payload - a `LatticeCredential` (`Token`, `Scheme`, `PrincipalId`,
`Metadata`) - directly. When no scope is entered, `Current` is
`null` and `IsActive` is `false` (a single dictionary lookup, no
allocation).

The marker carries a *user* credential only. Library-internal system /
maintenance / replication-origin calls are authored by infrastructure and
carry no credential: they never open a credential scope. When
infrastructure fans a system-origin sub-operation out from within a turn
that did carry a user credential, it wraps that sub-operation in
`LatticeCredentialContext.Suppress()` so the ambient credential is stripped
for the duration and cannot leak onto a system-authored call.

## Access-gate operation flags

A host that registers an `ILatticeAccessGate` receives one `LatticeOperation` flag for the logical operation being authorized. Composite calls can carry the union of several flags. The core flags are:

| Flag | Value | Authorizes |
|---|---:|---|
| `Read` | 1 | Single-key reads. |
| `Write` | 2 | Single-key writes. |
| `Delete` | 4 | Single-key deletes. |
| `RangeRead` | 8 | Contiguous range reads and scans. |
| `RangeDelete` | 16 | Contiguous range deletes. |
| `CrdtApply` | 32 | Applying a CRDT delta or merge to a key. |
| `AtomicWrite` | 64 | Initiating a multi-key or cross-tree atomic write. Individual legs still require their own write/delete grant. |
| `BulkLoad` | 128 | Bulk-load or snapshot-restore writes that populate a tree in bulk. |
| `Admin` | 256 | Routine tree administration. On `ILattice`: `SnapshotAsync`, `MergeAsync`, the per-tree event and history-retention overrides, `RebuildLeafProjectionAsync`, `CompactShardAsync`, and `RepairOrphanedLeavesAsync`; on the tree-administration facade: tree creation, alias assignment, and per-tree configuration updates. Existence checks are `Read`. |
| `Backup` | 512 | Capturing a tree, prefix, or key for backup. |
| `Restore` | 1024 | Restoring a captured backup into a target tree, prefix, or key. |
| `SchemaAdmin` | 2048 | Schema-management changes. |
| `Telemetry` | 4096 | Cluster-wide telemetry reads. |
| `Replication` | 8192 | Runtime replication-management operations. |
| `TreeLifecycle` | 16384 | Destructive or structural whole-tree lifecycle operations: drop, recover, and purge (`DeleteTreeAsync`, `RecoverTreeAsync`, `PurgeTreeAsync`), resize and undo-resize, reshard, WAL-placement moves, or orphaned-leaf repair through the tree-administration facade. |
| `AppInstall` | 32768 | Changing the cluster's installed app set: installing, upgrading, enabling, disabling, and uninstalling an [installable app](../../lattice.apps/README.md). Scopeless and cluster-wide, like `Telemetry`. |

`SchemaAdmin`, `Telemetry`, `Replication`, `TreeLifecycle`, and `AppInstall` are deliberately separate from `Admin`; granting one does not imply any other capability.

## Reading an empty range read under a gate

A denied read does not throw. A denied **point** read reports the key as absent,
and a denied **range** read resolves to a reject-all key filter and returns a
clean, successful, **empty** result. So
`KeysAsync`, `EntriesAsync`, their predicate overloads, both `CountAsync`
overloads, and the snapshot cursors all report "you may not look here" and "there
is nothing here" identically - no exception, no log, every instrument healthy.

That is deliberate. A denied scan stays cheap and non-fatal, and making it throw
would break every existing caller. The cost is that **emptiness alone is
uninterpretable under a gate**, so a caller that draws a conclusion from an empty
range read must confirm the range was actually readable:

```csharp verify
static async Task<bool> RangeIsGenuinelyEmptyAsync(
    ILattice tree, string startInclusive, string endExclusive, CancellationToken ct)
{
    await foreach (var key in tree.KeysAsync(startInclusive, endExclusive, cancellationToken: ct))
    {
        return false; // Not empty at all.
    }

    // Empty. Ask whether that is a fact about the store or about authorization.
    var coverage = await tree.GetRangeReadGateCoverageAsync(startInclusive, endExclusive, ct);
    return coverage == LatticeRangeReadGateCoverage.Unrestricted;
}
```

`GetRangeReadGateCoverageAsync` reports `Unrestricted`, `Filtered`, or `Denied`.
Only `Unrestricted` licenses reading emptiness as absence: under `Filtered` an
unknown subset of keys is withheld, and under `Denied` every key is. It is a
coverage classification and never names the withheld keys, for the same reason
`GatedMultiReadResult.PrunedByAccessGate` is a count - identities would make any
range read an authorization oracle.

Call it **only when a range read came back empty** and you are about to act on
that emptiness. The scan hot path pays nothing.

A background component is the classic victim, because its turn carries no caller
credential at all: under a fail-closed gate every one of its scans returns empty,
so it concludes the store is empty and does nothing, forever, with a healthy log
at every layer. If a component reads on a background turn, give it a credential
(see the trusted system-origin scope below) rather than relying on this check.
## Trusted system-origin scope

A co-hosted infrastructure extension that must run a trusted, gate-bypassing
sub-operation on a caller's behalf - for example the `Orleans.Lattice.Api.Mcp`
server resolving a caller's effective permissions before advertising tools -
enters that scope through the public `LatticeSystemOrigin` seam rather than
reaching into the library's internals:

```csharp verify
using (LatticeSystemOrigin.Enter())
{
    // Nested calls in this scope are treated as system-origin and admitted by
    // the access gate without a user identity. LatticeSystemOrigin.IsActive
    // reports whether such a scope is currently open.
    await tree.SetAsync("k", new byte[] { 1 }, cancellationToken);
}
```

Entering the scope bypasses the access gate for its lifetime, so it is reserved
for co-hosted code that already runs inside the silo's trust boundary. It is the
public counterpart of the internal marker the first-party add-ons use, and it
exists so an out-of-package extension need not take an internal-visibility grant
into the core assembly to perform the bypass.

Previous: [Mutation observers](mutation-observers.md). Next: [WAL saturation back-pressure](wal-saturation-back-pressure.md). Contents: [Lattice Public API Reference](../api.md).
