AppSurface Search
Guide

Logical configuration keys

Source of truth

View source Edit this page

Last updated

An AppSurface key has one identity across configuration providers. Payments:ApiKey and payments:apikey compare equal; Payments:Api-Key and Payments:Api:Key do not. Start with the Config quickstart, then use this reference when declaring keys or moving a setting between providers.

Grammar and construction

Only : separates hierarchy. Every segment is nonempty and contains no colon, control character, or leading/trailing whitespace. Interior whitespace and non-control Unicode remain unchanged. There is no trimming, culture-sensitive comparison, Unicode normalization, or arbitrary logical-key length limit. Each provider has separate native representation limits.

Input Meaning
Payments:ApiKey Two segments
Logging:LogLevel:Microsoft.Hosting.Lifetime Three segments; the dots are literal
Payments:Api-Key Two segments; the hyphen is literal
A_B, A__B, A/B, A\B One literal segment each; convention support varies
A B One segment with interior whitespace
Empty, :A, A:, A::B, A, A Invalid
var key = AppSurfaceConfigKey.Parse("Payments:ApiKey");
var literal = AppSurfaceConfigKey.FromSegments("Microsoft.Hosting.Lifetime");
var nested = AppSurfaceConfigKey.FromSegments("Logging", "LogLevel", literal.Value);

Parse uses strict grammar and throws FormatException for invalid input. TryParse returns false and a null output on failure. FromSegments copies its array; null arrays throw ArgumentNullException, and empty arrays or invalid members throw ArgumentException naming segments and the invalid member index. An element is a literal segment: passing "A:B" as one element is invalid.

Value and ToString() preserve spelling. Segments is immutable. Equality and hashing use StringComparer.OrdinalIgnoreCase; there is no lowercase canonical text. IsSameOrDescendantOf compares complete segments: Payments matches itself and Payments:ApiKey, but not PaymentsArchive:ApiKey. Deterministic reports sort by ordinal ignore-case rendering and then ordinal spelling.

Application strings and declarations

IConfigManager.GetValue<T>(environment, typedKey) resolves a typed identity. The string overload parses once using AppSurfaceConfigKeyOptions. In release train 1, LegacyDotPathBehavior.TranslateDotOnlyWithDiagnostic is the default: Payments.ApiKey becomes Payments:ApiKey and produces config-key-legacy-dot-path. A string containing a colon is strict, so its dots stay literal. Use the typed API for one literal dotted segment during this train. The migration guide describes the strict-default and removal trains.

services.Configure<AppSurfaceConfigKeyOptions>(options =>
    options.LegacyDotPathBehavior = LegacyDotPathBehavior.Strict);
services.AddConfigAuditKey<string>(AppSurfaceConfigKey.Parse("Payments:ApiKey"));

Attributes compose nested paths with colons. Public ConfigKeyAttribute.GetLogicalKey(Type) is strict. Module discovery parses raw attribute fragments using finalized application options, preserving migration origin through wrappers and requests. Root ignores declaring-type ancestors. GetKeyPath(Type) is obsolete and returns a colon rendering.

String AddConfigAuditKey<T> registrations defer parsing until the service provider exists, so later options registrations are honored. The immutable declaration registry belongs to one host. Exact canonical declarations merge audit metadata; case-only declarations and legacy/canonical declarations for one identity fail startup with config-key-collision. Direct obsolete ConfigAuditKnownEntry(string, …) construction is strict because it cannot consult DI options.

Explicit type registration

AddAppSurfaceConfig<TConfig>() selects one concrete IConfig wrapper from any compatible assembly, including a Domain assembly outside the normal discovery inputs. It uses the same declaration registry and finalized key parser as module discovery. It does not scan an assembly, accept a caller key, or parse an attribute while services are being registered. Without ConfigKeyAttribute, the existing type-name and declaring-type hierarchy convention supplies the declaration. With an attribute, nested fragments compose under the same Root rule as discovered wrappers.

ConfigKeyAttribute.GetLogicalKey(Type) remains a strict public helper: it parses attribute fragments with canonical colon syntax regardless of host compatibility options. Host discovery and explicit registration instead resolve those fragments through the finalized AppSurfaceConfigKeyOptions, so release-train legacy handling and origin tracking remain host-owned. Do not call the strict helper to predict a legacy-translated host key. The migration guide documents the package and rollback constraints for adopting this API.

Malformed fragments keep the parser's existing exception boundary: strict AppSurfaceConfigKey.Parse throws FormatException, while the default finalized ConfigKeyInputParser throws ArgumentException for invalid input after its configured dot-only translation. A custom input parser retains its own exception behavior. Canonical/case-only and legacy/canonical identity collisions still fail with InvalidOperationException and config-key-collision when the finalized declaration registry is constructed.

Registration contributes an attributed declaration even when the wrapper assembly is not scanned. That record identifies the selected wrapper for normal audit reporting; it is not evidence that the runtime singleton has been activated. Normal module discovery remains lazy. An audit report may separately construct an inspection wrapper and resolve providers, so keep report-time inspection and reads separate from runtime singleton activation in counters and troubleshooting.

Invalid attribute grammar and key collisions still fail through the existing finalized parser/registry boundary. Invalid audit traversal bounds also retain their existing later audit diagnostic and safe fallback; explicit registration does not turn them into an earlier key-registration failure.

The registration conflict code is config-registration-conflict. Its canonical documentation URI is https://appsurface.dev/guides/config-logical-keys, the same target as ConfigDiagnosticCatalog.Reference. Messages identify the problem and cause, safe wrapper identifier, actual exact-unkeyed descriptor count or lifetime, repair action, and canonical link. They never invoke opaque factories or include configuration values.

Registration failure Family and recovery
Null collection/type ArgumentNullException; provide the required value before registration.
Unsupported selected type shape ArgumentException; select a closed concrete class implementing IConfig with a public instance constructor.
Multiple exact unkeyed descriptors or a non-singleton InvalidOperationException with config-registration-conflict; retain one unkeyed singleton or repair descriptors before another registration call. Keyed descriptors are independent.
Incompatible package family AppSurfacePackageCompatibilityException; upgrade the coordinated package set and rebuild external providers and wrappers together.
Invalid declared key grammar ArgumentException from the default finalized ConfigKeyInputParser; strict AppSurfaceConfigKey.Parse throws FormatException. Correct the declaration; custom parser exceptions retain their existing behavior.
Case-only or canonical/legacy identity collision InvalidOperationException with config-key-collision when the finalized registry is constructed; use one spelling for each logical identity.

The canonical packed consumer proof demonstrates that a package-only Domain wrapper gains its attributed audit identity without broadening discovery.

Resolution and collision domains

The environment provider runs first. Other providers run in descending priority, with stable registration order breaking priority ties. Missing continues traversal. Found returns a value, including valid value-type defaults such as zero or false. Terminal retains one diagnostic and stops lower-provider traversal. The environment patcher then has one transactional chance to rescue a terminal result using valid present children. A failed child rejects the entire patch. Without a successful patch, the manager throws ConfigurationResolutionException.

Compatibility notices from environment child patches are published only when the whole patch succeeds. The manager logs each committed notice once using its child key and provider, and attributes the patched result to the environment provider. A failed or unapplied candidate does not emit successful-alias notices or report a missing base provider as resolved.

This terminal rule also applies when a provider cannot represent a key or capture its source. Register an explicit mapping when a provider's convention cannot represent a valid logical identity. An upper provider's failure is not converted to absence merely because a lower provider could supply a value.

Arrangement Outcome
Exact duplicate inside one source layer Terminal duplicate/collision
Case variants inside one layer Terminal collision
Canonical and distinct legacy aliases inside one layer Terminal collision, even if values match
Exact spelling in later documented layer Intentional override; origins retained
Case-only spelling difference across ordered layers Terminal collision
Same logical key in separate providers Normal manager precedence
Different logical keys mapped to one native identity Unrepresentable/collision before value retrieval

The declaration spelling represents a known key in reports. Otherwise, the winning provider's spelling represents it. Source records retain their own exact spellings and native locators. Neither logical equality nor report ordering rewrites native Google resources, LocalSecrets backend identifiers, file names, or environments. Expanded object members keep display labels such as Payments:Settings.Database.Host, and collection entries use brackets such as Payments:Services[0].Name. Those labels are presentation only. Provider matching uses the corresponding colon source path, including literal segment punctuation, so labels must not be parsed as logical keys.

Captured ConfigAuditEntry records include ConfigPath, the colon rendering used for logical comparison independently of Key's display label. This keeps a literal A.B distinct from the expanded member at A:B, even when both display as A.B. Paths containing hidden dictionary identifiers omit ConfigPath; dictionary HMAC comparison metadata continues to govern those entries. Older or manually authored child records without logical paths retain structural display evidence and cannot prove logical equivalence from display punctuation alone. When captures mix child records with and without ConfigPath under one logical root, those child items are Uncomparable with config-diff-logical-path-evidence-missing; metadata upgrades do not imply that children were added or removed. Root Key strings already carry strict logical identity and remain comparable. Regenerate captured reports to obtain the logical-path evidence. Source spellings still participate in provenance comparison, while a change in logical-key casing alone is not a value change.

Provider encodings

JSON properties contribute literal segments; array positions contribute numeric segments. A property with a colon cannot be represented as one logical segment. Raw duplicate properties and case collisions poison the affected identity and its aggregate ancestors. Unrelated siblings remain available. Arrays replace arrays as a unit; scalar/object/array replacement removes superseded descendant provenance. Null remains missing; empty objects and arrays are present.

Environment convention names uppercase ASCII segment spelling and join hierarchy with __. Payments:Api-Key becomes PAYMENTS__API-KEY; the hyphen is preserved. Convention segments allow ASCII letters, digits, ., -, /, \, and single interior underscores. Edge underscores, __ inside a segment, and non-ASCII content require an explicit AppSurfaceEnvironmentConfigOptions.MapKey mapping. A key whose unscoped form starts with the current environment's scoped prefix also requires a mapping, preventing Production:Payments:ApiKey from sharing a native name with scoped Payments:ApiKey.

An explicit suffix replaces convention and legacy candidates in both layers. The scoped <ENVIRONMENT>__<SUFFIX> wins over unscoped <SUFFIX>, after both layers pass collision validation. Explicit suffixes accept ASCII letters, digits, and single interior underscores only. They retain exact caller-owned casing.

Suffix Accepted
PAYMENTS_API_KEY, Payments_ApiKey Yes
PAYMENTS__APIKEY, _PAYMENTS, PAYMENTS_ No
PAYMENTS-APIKEY, PAYMENTS.APIKEY, non-ASCII text No

For every expected native name, one exact match resolves; one case-only match is config-key-environment-name-case; multiple case variants are config-key-collision. These rules are identical for Unix and Windows snapshots. One top-level operation uses one immutable snapshot for direct values, collections, patching, aliases, and audit. Discovery considers known declarations and mappings, not unrelated process variables.

Inspect environment candidate names

IEnvironmentConfigCandidateInspector reports ordered names that could be considered by native environment lookup or direct composition lookup. It is a names-only projection: it does not read environment variables or files, call providers, resolve or convert a value, establish presence or source ownership, prove aggregate completeness, or consume native claim capacity. The caller owns the inventory, its trust and comparer, platform presence checks, and the acceptance policy. See the packed package consumer proof. Adopt it with coordinated package versions selected by the package chooser and follow the coordinated migration guide; candidate package proof is not published-feed or release evidence.

Configure AppSurfaceEnvironmentConfigOptions and every other host option before build or first resolution. The provider and inspector freeze the same mapping snapshot on first use in one service container. Resolve the inspector after host configuration is final; a mapping changed after the first provider/inspector use is stale for that container and requires correcting configuration and rebuilding it. Directly constructed providers have isolated snapshots.

Start with an existing AppSurface host and the .NET 10 SDK. For this standalone snippet, run from an owned project directory. On macOS, set DOTNET_USE_POLLING_FILE_WATCHER=1 if the host's default file watching blocks in a restricted development environment, as the packed consumer verifier does. This host setup is separate from inspector source activity.

using ForgeTrust.AppSurface.Config;
using ForgeTrust.AppSurface.Core;
using ForgeTrust.AppSurface.Core.Defaults;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;

var builder = Host.CreateApplicationBuilder(args);
var environment = new MetadataExampleEnvironment();
builder.Services.AddSingleton<IEnvironmentProvider>(environment);
var context = new StartupContext([], new NoHostModule(), EnvironmentProvider: environment)
{ OverrideEntryPointAssembly = typeof(NoHostModule).Assembly };
new AppSurfaceConfigModule().ConfigureServices(context, builder.Services);
foreach (var register in context.CustomRegistrations) register(builder.Services);

// Finalize the environment mapping before Build or any provider/inspector resolution.
builder.Services.Configure<AppSurfaceEnvironmentConfigOptions>(options =>
    options.MapKey("Payments:ApiKey", "Approved_ApiKey")
        .MapKey("Feature:Endpoint", "Feature_Endpoint")
        .MapKey("Service", "Root_Config")
        .MapKey("Service:ApiKey", "Service_ApiKey"));
using var host = builder.Build();
var inspector = host.Services.GetRequiredService<IEnvironmentConfigCandidateInspector>();

var typed = inspector.InspectNativeKey("Production", AppSurfaceConfigKey.Parse("Payments:ApiKey"));
var strict = inspector.InspectNativeKey("Production", "Payments.ApiKey");
var path = inspector.InspectCompositionPath("Production", AppSurfaceConfigKey.Parse("Payments:ApiKey"));

// Caller policy: reject any supplied environment name that could override this setting.
var inventory = new HashSet<string>(["Approved_ApiKey"], StringComparer.Ordinal);
var rejected = typed.Candidates.Any(candidate => inventory.Contains(candidate.Name));
if (!rejected || environment.Reads != 0)
    throw new InvalidOperationException("Unexpected candidate-policy or source-read result.");
Console.WriteLine($"Supplied environment override rejected: {rejected}; inspector source reads: {environment.Reads}");

try
{
    inspector.InspectNativeKey("Production", "Payments::ApiKey");
}
catch (FormatException failure) when (failure.Data["Code"] as string == "config-key-invalid")
{
    Console.Error.WriteLine($"{failure.Data["Code"]}: {failure.Message}");
    Console.Error.WriteLine($"Recovery: {failure.Data["Recovery"]}");
    Console.Error.WriteLine(failure.HelpLink);
    Console.Error.WriteLine(failure.Data["Fix"]); // Keep the actionable fix last.
}

sealed class MetadataExampleEnvironment : IEnvironmentProvider
{
    public string Environment => "Production";
    public bool IsDevelopment => false;
    public int Reads { get; private set; }
    public string? GetEnvironmentVariable(string name, string? defaultValue = null)
    {
        Reads++;
        throw new InvalidOperationException("Metadata inspection must not read a value.");
    }
    public IReadOnlyDictionary<string, string> CaptureEnvironmentVariables()
    {
        Reads++;
        throw new InvalidOperationException("Metadata inspection must not capture values.");
    }
}

The snippet prints Supplied environment override rejected: True; inspector source reads: 0. This counter covers the supplied AppSurface environment source during inspector construction/calls; normal host construction and application callbacks remain outside that boundary.

The regular module graph registers the inspector as a singleton; do not register it manually. The standalone consumer fixture uses StartupContext, AppSurfaceConfigModule.ConfigureServices, and CustomRegistrations to prove the public package route. The string overload uses strict colon grammar, so "Payments.ApiKey" is one literal segment. It does not apply a manager migration option that translates dot-only application strings.

For the fixture mapping above, the typed result is exactly PRODUCTION__Approved_ApiKey (scoped, explicit mapping) followed by Approved_ApiKey (unscoped, explicit mapping). Both have IsLegacyAlias == false. The strict-string input "Payments.ApiKey" is a distinct one-segment key; it yields PRODUCTION__PAYMENTS.APIKEY, PAYMENTS.APIKEY, PRODUCTION_PAYMENTS_APIKEY, PRODUCTION__PAYMENTS_APIKEY, and PAYMENTS_APIKEY, in that order. Only the final three are IsLegacyAlias == true; every candidate has IsExplicitMapping == false. For direct composition path Payments:ApiKey, the names are PRODUCTION_PAYMENTS_APIKEY, PAYMENTS_APIKEY, PRODUCTION__PAYMENTS__APIKEY, and PAYMENTS__APIKEY; the first two are flat (IsHierarchical == false) and the latter two hierarchical. These plans use their distinct entry-point semantics despite the similar-looking strings. Without an explicit mapping, typed keys produce canonical names only. Strict string inspection preserves compatibility aliases for its strict-string origin, while a typed key never gains those aliases. Composition inspection applies its direct-path rules and does not use native explicit mappings. Scope records the producing layer; IsHierarchical describes the composition projection. Exact duplicate names are removed ordinally without case folding.

Canonical output and first producer

For Production and typed Service:Api-Key, native inspection yields:

Name Scope Legacy alias Explicit mapping
PRODUCTION__SERVICE__API-KEY Scoped false false
SERVICE__API-KEY Unscoped false false

The composition projection is a distinct legacy table:

Name Scope Hierarchical
PRODUCTION_SERVICE_API_KEY Scoped false
SERVICE_API_KEY Unscoped false
PRODUCTION__SERVICE__API__KEY Scoped true
SERVICE__API__KEY Unscoped true

First-producer metadata survives ordinal duplicate removal. For path Service, the surviving sequence is PRODUCTION_SERVICE (scoped flat), SERVICE (unscoped flat), and PRODUCTION__SERVICE (scoped hierarchical). The later duplicate SERVICE does not replace the flat candidate's IsHierarchical == false flag. Mapping exact key Service to Root_Config yields PRODUCTION__Root_Config and Root_Config with explicit mapping flags, but does not rewrite Service:ApiKey or its composition plan.

Composition projection is deliberately not injective: literal paths made by FromSegments("A.B"), FromSegments("A-B"), and FromSegments("A", "B") can share A_B and A__B candidates, although their typed native identities differ. A/B and A_B retain their existing separate rules. Do not use composition names as logical key identity.

The package fixture demonstrates two caller-owned environment rejection rules. For a file-only Feature:Endpoint, the caller rejects the setting when either planned native name matches a forbidden-environment inventory. For Service:ApiKey, the caller applies the same rule while a fake approved provider is registered; its Resolve is never called by inspection. Both rules use actual HashSet<string> membership. Their mixed case explicit suffixes make comparer behavior visible: an ordinal set misses a lowercased environment name, while an ordinal-ignore-case set finds the candidate and rejects the override. To show ancestor dependence, a separate container maps Service to Root_Config only. Inspecting Service:ApiKey alone misses forbidden PRODUCTION__Root_Config; adding the Service ancestor plan detects it. The fixture checks these decisions against caller-provided environment-name inventories; it does not read platform variables or claim source ownership.

Entry point Input interpretation Candidate source
InspectNativeKey(environment, AppSurfaceConfigKey) Typed identity, irrespective of hidden parser provenance Canonical encoding or exact explicit mapping; no aliases
InspectNativeKey(environment, string) Strict AppSurfaceConfigKey.Parse grammar Native encoding and strict-string compatibility aliases
InspectCompositionPath(environment, AppSurfaceConfigKey) Supplied literal path segments Direct composition projection; native mappings do not apply

Do not replace an existing policy inventory until its expected names and flags have been checked against the actual deployment contract. Caller-owned inventories may use ordinal case-sensitive or case-insensitive matching. Those choices can change whether a mixed-case explicit suffix is accepted; they never change the inspector's output. Inspect relevant roots, ancestors, and known member paths explicitly: an exact mapping for an ancestor does not rewrite a descendant native plan. Direct path projections may collapse repeated or empty path components according to the existing composition rules. Input size and number of requested paths are caller-controlled and unbounded by this API; bound batches in application policy if names come from untrusted or large inputs. Every result is materialized before return. Calls allocate in proportion to key and environment spelling length; freezing a container copies its configured mapping set, so snapshot storage also grows with mapping count. Inspection keeps no per-key cache, claim history, or retained request state. Prefer startup and explicit policy checks over unbounded repeated batches. No latency or memory budget is guaranteed; bounded allocation observations in tests establish correctness under representative inputs, not a production performance promise.

Candidate inspection errors

Framework-owned known failures retain their original CLR exception and carry string Exception.Data fields Code, Operation, Fix, Recovery, SideEffects, and HelpUri, plus HelpLink. These fields contain parameter/option names and repair guidance, never raw keys or secret values. Framework inspector failures have SideEffects set to none. A strict parse failure retains FormatException and config-key-invalid; null input/DTO guards use config-candidate-null; native unencodable keys use config-candidate-unencodable; invalid frozen options use config-candidate-options-invalid.

Use the specific anchors for null input or DTO rejection, native unencodable key, invalid frozen options, and strict key grammar; this section is the overview.

try
{
    var plan = inspector.InspectNativeKey("Production", untrustedKey);
    ApplyCallerPolicy(plan.Candidates);
}
catch (FormatException failure) when
    (failure.Data["Code"] as string == "config-key-invalid")
{
    RenderSafeFailure(failure.Message, failure.Data["Code"], failure.Data["Operation"],
        failure.Data["Recovery"], failure.Data["SideEffects"], failure.HelpLink,
        failure.Data["Fix"]); // Render Fix last so it remains the final user action.
}

Correct invalid syntax and retry; repair an unencodable key or configure its exact mapping before construction rather than retrying unchanged input. Invalid mapping options require repairing the named validation failure and rebuilding the container. For an unexpected failure, stop and capture package version, operation, and a sanitized reproduction; do not blindly retry. Its canonical guide anchor is config-candidate-unexpected. The framework does not promise enrichment or redaction for arbitrary host callbacks, whose exceptions and side effects remain host-owned. Candidate plans are immutable copies, but no refresh or mapping mutation hook is provided after snapshot freeze.

API reference and upgrade notes

The public XML reference is attached to EnvironmentCandidateScope, EnvironmentNativeCandidate, EnvironmentNativeKeyPlan, EnvironmentCompositionPathCandidate, EnvironmentCompositionPathPlan, and IEnvironmentConfigCandidateInspector in the Config assembly. Plan constructors and with { Candidates = list } copy to immutable arrays and reject null lists/elements; generated deconstruction returns the immutable public list. Plain with { } copies may share immutable backing values. A new typed overload and new inspector interface are additive API surface, but callers should use the coordinated Config/Core package versions established by release evidence. A packed package test proves a locally packed candidate; it does not prove that a published feed contains that version.

Null input and DTO validation

Supply a non-null argument, key/path, candidate collection, or collection element. The original ArgumentNullException or ArgumentException identifies its parameter; no source lookup is attempted.

Unencodable native key

Correct the key or configure its exact native mapping before first construction. The existing InvalidOperationException is retained; unchanged retries cannot succeed.

Invalid frozen options

Repair the named AppSurfaceEnvironmentConfigOptions failure and rebuild the service container. The original OptionsValidationException and its validation failures are retained.

Strict logical key grammar

Use nonempty colon-delimited segments without controls or edge whitespace. Strict parsing preserves its existing FormatException and grammar message.

Unexpected inspector failure

Stop rather than retrying blindly. Capture package version, operation, and a sanitized reproduction for maintainers. Framework-known errors are enriched in place; arbitrary host callback exceptions and side effects are outside that guarantee.

LocalSecrets compares logical keys case-insensitively while retaining exact stored identifiers. A write using another case updates the existing entry. Canonical writes no longer transform double underscores or backslashes. Existing aliases require the explicit migration flow; reads never rename or delete a secret.

Google Secret Manager retains exact explicit resource names. Conventions claim strict descendants of a segment prefix and encode the full key: lowercase segments joined with --, preceded by the exact configured secret-id prefix. Segments match [A-Za-z0-9_]+(?:-[A-Za-z0-9_]+)*; dots, unsupported Unicode, edge hyphens, and -- inside a segment require explicit mappings. All conventions share one nonempty, ordinal-exact generated prefix. Final secret IDs must satisfy Google's native grammar and 255-character limit. Resource identity includes project and version.

Bounds and diagnostic trust

ConfigResourceOptions limits are positive and configurable before host creation. Defaults are 16 MiB per JSON file, 256 files per environment, 65,536 environment entries, 64 MiB of UTF-8 environment names plus values, 4,096 retained notice identities, 256 rendered identifier characters plus a digest, and binding depth 32. Exceeding a source limit fails closed without publishing a partial snapshot. When an operation reaches its notice capacity, it retains the bounded first notices and marks audit evidence incomplete with config-audit-notice-limit. Process-level log suppression remains separately bounded and does not remove notices from an otherwise complete report.

Google payload caching, when enabled through its TTL option, defaults to a capacity of 1,024 expiring entries. Its native claims and audit work have separate validated bounds; the aggregate audit defaults are ConfigResourceOptions.AuditTimeout (30 seconds), MaxAuditRemoteLookups (256), and MaxAuditConcurrency (4). An exhausted audit budget is explicitly incomplete. Caller cancellation stops that waiter without cancelling another waiter's shared fetch.

Expected diagnostics have Code, Problem, Cause, Fix, Docs, and Retryable. Built-in templates accept safe identifiers only. Automatic manager logs contain reviewed codes and bounded identifiers, never arbitrary third-party diagnostic prose, configuration values, secret payloads, or raw provider exceptions. Logger failures do not alter resolution. See the provider-author guide for thread safety, result factories, conformance, and extension responsibilities.

The ForgeTrust.AppSurface.Config meter emits appsurface.config.terminal and appsurface.config.notice counters. Their only tags are reviewed code and provider categories; unknown providers become custom. Notice counts occur before log suppression to expose migration usage. There are no key, environment, source identifier, or value tags. Listener failures cannot change resolution.

ConfigAuditTextRenderer and ConfigAuditDiffTextRenderer escape control and format characters in identifiers and apply MaxRenderedIdentifierCharacters before display. Their parameterless constructors use the defaults; the IOptions<ConfigResourceOptions> constructors copy validated limits when created. Explicit diagnostic prose is escaped and capped at 4,096 characters without hashing its content. Identifier shortening is presentation only: structured source records retain exact locators for matching and comparison. Renderers consume already-redacted display values; they do not make an untrusted provider's raw value or prose safe to publish.