Configuration Reference
Every knob the harness exposes — what it does, where it lives in code, and the scenarios in which you'd actually change it. This is a reference chapter: it is long by design, and it is not meant to be read cover to cover.
Most people arrive here with a specific goal, not a desire to learn the config schema. If that's you, skip ahead:
- "I want to change something specific" — go straight to the "I want to…" recipes near the bottom. Switching models, turning on RAG, tightening permissions, quieting logs: each is a short worked example with the exact JSON.
- "My config isn't taking effect" — read Where configuration comes from first. It is almost always precedence, and the answer is two minutes away.
- "I need to know what's available" — skim The AppConfig tree, then jump to the one section you need. Every section below is independent; nothing builds on what came before.
- "I'm adding my own config" — the last two sections (validation and where the classes live) are the ones you want.
The whole configuration system on this page lives in Domain.Common, which
means it is shared by every fork of this template. Your own application settings — a
payment key, a partner endpoint — are a separate concern. You can either hang them off
AppConfig or create a parallel config root in your own project; the
pattern (a root POCO bound at startup via
services.Configure<T>()) works either way. Chapter 11 covers
that choice when you come to make it.
Where configuration comes from
The harness uses standard .NET configuration. Values are pulled from three sources in this priority order — later sources override earlier ones:
-
appsettings.json— the default values, checked into source control. This is the file you'll spend the most time in. -
appsettings.Development.json— environment-specific overrides (only loaded whenASPNETCORE_ENVIRONMENT=Development, which is the default duringdotnet run). - User Secrets (dev) or environment variables / Azure Key Vault (prod) — for anything sensitive: API keys, connection strings, signing secrets. These override the JSON files.
Defaults in JSON make the codebase self-documenting and reproducible. Per-environment JSON lets you tweak without forking. Secrets live outside the repo so you can't accidentally commit an API key. Same code runs on your laptop, in CI, and in production — only the configuration sources change.
How the values get into your code
Every key under AppConfig in JSON maps to a property on a strongly-typed C# class
— the root is AppConfig.cs in Domain.Common. Binding happens once at
startup:
services.Configure<AppConfig>(configuration.GetSection("AppConfig"));
From then on, any service can ask for an IOptionsMonitor<AppConfig>:
IOptionsMonitor<T>, not
IOptions<T>. IOptionsMonitor picks up changes to
appsettings.json at runtime without a restart. IOptions
snapshots once at startup and never refreshes.
CurrentValue returns the latest bound instance. Cache it in a local if you
read it many times in one method.
IOptionsMonitor only re-binds when the underlying file changes. If a
service captures a value in a constructor or holds onto an instance after a read, that
copy goes stale. The safe pattern is to always read CurrentValue at the
top of each method that needs it.
The AppConfig tree
AppConfig is the single root. Everything else hangs off it. Here's the full
top-level shape — each child is a section you can configure independently:
Every node above has a corresponding class in
src/Content/Domain/Domain.Common/Config/. If you can't find what a key controls,
open the class — XML doc comments explain each property.
AppConfig.Common
General settings that apply across all layers.
| Key | Type | Default | What it does |
|---|---|---|---|
ApplicationName |
string | "AgenticHarness" |
Used as the OpenTelemetry resource attribute and in health-check output. |
ApplicationVersion |
string | "1.0" |
Stamped onto OTel resources and API version metadata. |
SlowThresholdSec |
int | 5 |
Any MediatR request that exceeds this duration is logged as a warning by
RequestPerformanceBehavior. Lower it to surface slowness more
aggressively in load tests.
|
AppConfig.Logging
Controls where logs are written and how they're formatted.
| Key | Default | What it does |
|---|---|---|
LogsBasePath |
"logs" |
Base directory for JSONL log files. Each run gets its own subfolder. Relative paths resolve from the application working directory. |
PipeName |
"agentic-harness-logs" |
Windows named-pipe name used to stream logs to LoggerUI in real
time. Different for each Presentation host (ConsoleUI uses
AgenticHarnessLogs.ConsoleUI).
|
EnableStructuredJson |
true |
Whether to emit JSONL log files. Disable only for tiny dev environments. |
RingBufferCapacity |
500 |
Last N log entries kept in-memory for the diagnostics endpoint. Independent of on-disk retention. |
SuppressConsoleOutput |
false |
Set to true when running LoggerUI alongside, so the agent console
stays clean and logs flow only through the pipe.
|
AppConfig.Agent
Defaults applied to every agent conversation. Most sub-systems can override per agent.
| Key | Default | What it does |
|---|---|---|
DefaultRequestTimeoutSec |
30 |
Cancellation budget for any MediatR request that doesn't specify its own.
TimeoutBehavior enforces it.
|
DefaultTokenBudget |
200000 |
Cap on tokens the agent can spend per turn — across system prompt,
loaded skills, tool schemas, and history. TokenBudgetBehavior enforces
it. Bound at AppConfig:AI:AgentFramework:DefaultTokenBudget. See
Skills for how budget interacts with progressive
disclosure.
|
ConversationTokenBudget |
1000000 |
Cross-turn ceiling for the whole conversation, enforced by the
IConversationBudgetTracker singleton — distinct from the per-turn
DefaultTokenBudget. When exhausted, the loop breaks gracefully. There
is no MaxTurnsPerConversation property; budgets are
token-based, not turn-count-based. The default is roughly 50–100 exchanges once
each turn's resent history is counted — a runaway guard, not a limit an ordinary
conversation meets. Set it to 0 to disable, but note that
nothing else bounds a durable conversation's total length: the
per-run maxTurns and message caps deliberately do not.
Upgrading: this previously shipped disabled, so a deployment that
pins 0 in its own appsettings.json keeps that value and
stays unbounded — remove the pin to pick up the bound.
|
If your agent legitimately needs to load many large skills (e.g. it's an orchestrator
with a dozen sub-skills), bump DefaultTokenBudget. If the agent is
hitting the cap because it's loading too eagerly, fix the
skill tier instead — see Skills System.
AppConfig.Infrastructure.FileSystem
This is the sandbox that decides which paths agents can read or write. Everything outside the
allow-list is rejected at the service boundary, not at the LLM. Even if a malicious prompt
convinces the agent to cat /etc/passwd, the call fails before it leaves the
process.
"Infrastructure": {
"FileSystem": {
"AllowedBasePaths": [ "../../../../../../.." ]
}
}
| Key | What it does |
|---|---|
AllowedBasePaths |
Array of absolute or relative paths the agent is allowed to traverse. Relative
paths resolve from the running executable's directory — that's why the default
looks like a long ../ chain (it climbs from
bin/Debug/net10.0/ back up to the repo root).
|
"/"
The first instinct when a tool says "access denied" is to add "/" or
"C:\\" to the list and move on. Don't. The sandbox is
what stops prompt-injected exfiltration. Add the specific subfolder, never a root.
AppConfig.AI.AgentFramework
Which LLM provider the harness talks to, and which model.
| Key | Type | What it does |
|---|---|---|
ClientType |
enum |
One of seven AIAgentFrameworkClientType values:
AzureOpenAI | OpenAI | AzureAIInference |
PersistentAgents | Anthropic | Echo |
FoundryResponses. Selects which SDK ChatClientFactory
uses. (There is no OpenRouter value — point the OpenAI
client at an OpenAI-compatible Endpoint to use OpenRouter.)
|
Endpoint |
string |
Azure OpenAI resource URL (e.g.
https://my-resource.openai.azure.com/). Leave blank for plain OpenAI.
Always stored in User Secrets / Key Vault, never in JSON.
|
ApiKey |
string | API key for the provider. Always a secret. |
DefaultDeployment |
string | Name of the deployment (Azure) or model (OpenAI) used when no override is specified. Case-sensitive. |
AvailableDeployments |
string[] |
Allow-list of deployments callers can request per-conversation. Empty means
"DefaultDeployment is the only option."
|
AppConfig.AI.Skills and AppConfig.AI.Agents
Tell the harness where to find SKILL.md and AGENT.md files on disk.
These define what the agents know and do — covered in depth in
Skills System.
"Skills": {
"BasePath": "skills",
"AdditionalPaths": []
},
"Agents": {
"BasePath": "agents",
"AdditionalPaths": []
}
BasePath is the primary location. AdditionalPaths lets you layer
tenant- or environment-specific definitions on top of the built-ins — useful when you ship a
base set of skills and your customer adds their own without forking.
AppConfig.AI.McpServers
Registry of external MCP servers this app should connect to as a client. The
harness will discover their tools at startup and expose them to agents just like internal
tools. (For the inverse direction — exposing your tools to other MCP clients — see
AppConfig.AI.MCP and MCP Server & Client.)
"McpServers": {
"Servers": {
"bifrost": {
"Type": "Http",
"Url": "http://your-mcp-gateway:8090/mcp",
"Description": "Bifrost MCP gateway — github, firecrawl, playwright"
}
}
}
| Key | What it does |
|---|---|
Type |
Http | Stdio. HTTP for remote servers, Stdio for local processes. |
Url |
For HTTP servers — the MCP endpoint URL. |
Auth |
Optional. Type is None | ApiKey |
Bearer (static token in BearerToken) | Entra
(Azure managed identity — Scopes-only, via
EntraTokenAuthHandler). See McpServerAuthConfig.
|
Description |
Human-readable, surfaced in observability output. Doesn't affect behavior. |
AppConfig.AI.Governance
The policy engine that decides whether a tool call should run, be blocked, or escalate to a human. Reads policies from YAML files.
| Key | Default | What it does |
|---|---|---|
Enabled |
true |
Master switch. When off, every tool call bypasses policy checks. |
PolicyPaths |
["Policies/default-policy.yaml"] |
One or more YAML policy files to load. Relative paths resolve from the app base directory. |
ConflictStrategy |
PriorityFirstMatch |
What to do when multiple rules match — first match wins by priority. |
EnablePromptInjectionDetection |
true |
Run deterministic prompt-injection patterns over every user input. |
InjectionBlockThreshold |
High |
Low | Medium | High | Critical.
Detections at or above this level block. Lower-severity detections only
log.
|
EnableMcpSecurity |
true |
Scan MCP tool schemas at registration time for known dangerous shapes. |
EnableAudit |
true |
Tamper-evident audit log of every governance decision. |
EnforceToolInvocation |
false |
Opt-in switch that arms IToolInvocationGovernor — the live tool
authorization gate on the execution path (permission resolver → graded-autonomy →
capability → YAML policy). Off by default (closed-by-default enforcement is opt-in).
|
DataClassification.Mode |
Off |
Purview data-classification DLP gate (IToolClassificationGate):
Off | Audit | Enforce. In
Enforce, a Block verdict returns a model-facing message
instead of running the tool, and a RedactOutput verdict scrubs the
result. Requires a classification provider enabled.
|
Escalation |
— |
Nested config for human-in-the-loop approvals. Sets timeout per priority level
(Informational, Blocking, Critical) and
where audit records are written. See Observability & Safety.
|
AppConfig.AI.Permissions
Per-tool approval policies. Distinct from Governance: governance enforces org-level rules from YAML; permissions are operational guards on individual tool invocations, keyed by autonomy tier.
| Key | Default | What it does |
|---|---|---|
DefaultBehavior |
Ask |
Allow | Ask | Deny. Fallback when no rule matches. |
DefaultAutonomyLevel |
Supervised |
Restricted | Supervised | Autonomous.
Assigned to any subagent without an explicit level in its
SubagentDefinition.
|
TierPolicies |
— |
Per-tier overrides. Each entry has a DefaultBehavior and a
ToolOverrides map keyed by tool name. The
Autonomous tier typically defaults to Allow;
Restricted defaults to Ask with only safe reads
pre-approved.
|
DenialRateLimitThreshold |
3 |
After this many consecutive denials for the same pattern, future matches auto-deny. |
SafetyGatePaths |
.git/, .claude/, .ssh/, .env |
Paths that always require explicit approval regardless of other rules. |
AppConfig.AI.Resilience
LLM provider resilience — fallback chains, circuit breakers, retries, degraded mode. Off by default because most dev setups talk to one provider and want clear error messages rather than silent fallback. Turn it on for production.
| Key | What it does |
|---|---|
Enabled |
Master toggle. Off → no resilience pipeline, no retry queue. |
FallbackChain[] |
Ordered list. First entry is primary; subsequent entries are tried in order on
failure. Each entry declares its ClientType,
DeploymentId, and Capabilities (tool calling, streaming,
vision, max tokens). The orchestrator uses capabilities to skip providers that
can't satisfy a given request.
|
CircuitBreaker |
FailureRatio (0–1), SamplingDurationSeconds,
MinimumThroughput, BreakDurationSeconds. Polly v8
ratio-based breaker — when failure ratio is exceeded over the sampling window, the
circuit opens for the break duration.
|
Retry |
MaxAttempts, BaseDelaySeconds, BackoffType (Exponential | Linear | Constant).
Applies only to failures classified as transient — see
ErrorClassification below. When resilience is on, chain providers are
built with their SDK's own retry disabled so this is the only layer retrying.
|
ErrorClassification |
Decides what is retried, what counts against a provider's health, and what stops the
fallback chain. HTTP status mapping is built in: 429 and 5xx are transient;
401/402/403 are fatal for the whole chain (retrying a rejected key or an exhausted
balance wastes time, and rotating providers cannot fix shared configuration); 404 is
fatal for that provider only, so the chain still rotates. Providers that report a
billing failure as a 400 are caught by wording instead — add your provider's phrasing
via AdditionalChainFatalMessagePatterns or
AdditionalProviderFatalMessagePatterns. Both add to the built-in
patterns. Wording is only ever consulted for 4xx or status-less failures, so a broad
pattern cannot turn a genuine rate limit into a hard stop.
|
Timeout |
Per-attempt timeout. Distinct from the conversation-level timeout in AppConfig.Agent. |
DegradedMode |
What happens when every provider is exhausted: requests queue for retry up to
MaxQueueSize and live for RetryQueueTtlSeconds before
failing.
|
AppConfig.AI.Orchestration.Subagent
Controls multi-agent orchestration — when an orchestrator spawns sub-agents to delegate work.
| Key | Default | What it does |
|---|---|---|
MaxConcurrentSubagents |
3 |
Upper bound on parallel sub-agent invocations from a single orchestrator turn. |
DefaultMaxTurnsPerSubagent |
10 |
Cap on the inner agent loop within a delegation. |
MaxDelegationDepth |
3 |
How deep the orchestrator → sub → sub chain can go. Hard limit to prevent runaway recursion. |
CapabilityMatchWeights |
— |
Tunes the routing score: ToolCoverage + TypeAlignment +
TierHeadroom must sum to 1.0. Bias toward
TierHeadroom for safety-first routing.
|
MailboxStoragePath / DelegationStoragePath |
under .agent-sessions/ |
On-disk locations for inter-agent message storage and delegation audit records. |
AppConfig.Observability
OpenTelemetry pipeline configuration. See Observability & Safety for the conceptual model and the Jaeger setup.
| Key | What it does |
|---|---|
EnableSensitiveTelemetry |
When true, records sensitive GenAI content (prompt / completion text) in
traces. Defaults to false — only non-sensitive metadata (model, token
counts) is captured. Never enable in production without PII filtering and retention
policies in place. Tracing and metrics pipelines are always on; there are no
EnableTracing / EnableMetrics toggles.
|
SamplingRatio |
0.0–1.0. A collector-tier hint, not an in-app switch — the SDK exports all spans and
the OpenTelemetry Collector performs tail-based sampling (see
Observability). Left at 1.0 here.
|
PostgresConnectionString |
Persistent store for OTel-based audit trails. Optional but recommended. |
Exporters |
Sub-config for each destination — Jaeger / OTLP, Prometheus, Azure Monitor. Each
has its own Enabled flag and endpoint.
|
PiiFiltering |
Patterns redacted before any trace leaves the process. |
AppConfig.Cache
| Key | What it does |
|---|---|
CacheType |
Memory | Redis. In-process or distributed. |
RedisConnectionString |
Required when CacheType = Redis. Stored as a secret. |
AppConfig.MetaHarness
The self-optimization loop — proposes changes to skill files, evaluates them, keeps the best.
| Key | Default | What it does |
|---|---|---|
TraceDirectoryRoot |
"traces" |
Where JSONL execution traces are written for the proposer to grep. |
MaxIterations |
10 |
Upper bound on the propose-evaluate-record loop per run. |
EvalTasksPath |
"eval-tasks" |
Directory of JSON task definitions used to score candidates. |
MaxRunsToKeep |
20 |
Old run directories beyond this are pruned. |
EnableShellTool |
false |
Whether the proposer is allowed to invoke a shell. Leave off unless you know what you're doing. |
AppConfig.Azure
Activation is opt-in: every Azure service has its own Enabled flag or
connection-string check. Leave the keys blank and the harness simply doesn't wire up that
service.
ApplicationInsights— telemetry exporter for App Insights.AzureADB2C— JWT validation for the MCP server and AgentHub.AzureDatabase— SQL connection for persistent state.KeyVault— production secret source. Replaces User Secrets in non-dev.GraphApi— Microsoft Graph credentials, optional for M365 connectors.
"I want to..." — common reconfiguration recipes
If you're skimming for the most common adjustments, this is the section to read.
Switch from Azure OpenAI to OpenAI (or vice versa)
# Tell the harness which SDK to use
dotnet user-secrets set "AppConfig:AI:AgentFramework:ClientType" "OpenAI" \
--project src/Content/Presentation/Presentation.ConsoleUI
dotnet user-secrets set "AppConfig:AI:AgentFramework:ApiKey" "sk-..." \
--project src/Content/Presentation/Presentation.ConsoleUI
dotnet user-secrets set "AppConfig:AI:AgentFramework:DefaultDeployment" "gpt-4o" \
--project src/Content/Presentation/Presentation.ConsoleUI
Restart the app. ChatClientFactory picks the right SDK based on ClientType.
Let the agent read a folder outside the repo
Edit appsettings.Development.json (so it doesn't affect production):
"AppConfig": {
"Infrastructure": {
"FileSystem": {
"AllowedBasePaths": [
"../../../../../../..",
"C:\\projects\\my-other-repo"
]
}
}
}
Be specific — give it the exact folder, not a parent.
Raise the budget for long-running tasks
"AppConfig": {
"AI": {
"AgentFramework": {
"DefaultTokenBudget": 300000, // per-turn cap
"ConversationTokenBudget": 2000000 // cross-turn ceiling for the whole conversation
}
}
}
The harness has no fixed turn limit — it is governed by token budget. The per-turn
DefaultTokenBudget bounds a single turn; the cross-turn
ConversationTokenBudget (enforced by IConversationBudgetTracker)
breaks the loop gracefully when a long conversation exhausts it.
Add an external MCP server
"AppConfig": {
"AI": {
"McpServers": {
"Servers": {
"internal-tools": {
"Type": "Http",
"Url": "https://mcp.mycompany.internal/mcp",
"Description": "Company internal toolset",
"Auth": {
"Type": "Bearer",
"BearerToken": "${INTERNAL_MCP_TOKEN}"
}
}
}
}
}
}
The property is BearerToken (not Token). Use
${ENV_NAME} substitution for any secret — never inline it in JSON. The value
resolves from INTERNAL_MCP_TOKEN in environment or User Secrets at bind time.
For an Azure-hosted MCP server, use managed-identity auth instead of a static
token — set Type to Entra and supply Scopes
(EntraTokenAuthHandler acquires the token via McpConnectionManager):
"Auth": {
"Type": "Entra",
"Scopes": ["api://mcp-server-app-id/.default"]
}
Managed-identity config is Scopes-only: supplying a lone TenantId
without scopes is rejected at bind time.
Run more autonomously (skip "Ask" prompts)
"AppConfig": {
"AI": {
"Permissions": {
"DefaultAutonomyLevel": "Autonomous"
}
}
}
Autonomous means the agent runs tools without asking. Fine for an
automated job, dangerous for an interactive session with a curious user. Governance
rules still apply, but the per-call "are you sure?" prompts are skipped.
Enable LLM fallback in production
"AppConfig": {
"AI": {
"Resilience": {
"Enabled": true,
"FallbackChain": [
{ "ClientType": "AzureOpenAI", "DeploymentId": "gpt-4o", "Capabilities": { "SupportsToolCalling": true, "MaxTokens": 128000 } },
{ "ClientType": "AzureAIInference", "DeploymentId": "claude-sonnet", "Capabilities": { "SupportsToolCalling": true, "MaxTokens": 200000 } }
]
}
}
}
Quiet the logs in console output
If you're running LoggerUI alongside the ConsoleUI, set
SuppressConsoleOutput: true so logs flow through the pipe and only agent output
appears in the terminal:
"AppConfig": {
"Logging": {
"SuppressConsoleOutput": true
}
}
Validation and binding errors
Misconfigured values fail fast at startup, not at the first call. The harness validates every
config section in its Add*Dependencies() extension. If you mistype a key, you'll
usually get a clear message; if you set the wrong type (e.g. a string where an int is
expected), .NET binding throws on bind.
Put a breakpoint in App.cs after BuildServiceProvider(),
resolve IOptionsMonitor<AppConfig>, and inspect
CurrentValue. Every property should be populated; any
null where you expected a value is a binding miss — usually a typo in the
JSON key path.
Where every config class lives
One file per concept. The path is always Domain.Common/Config/...:
When in doubt, search for the JSON key in this directory:
grep -r "MyKeyName" src/Content/Domain/Domain.Common/Config/. The property name
almost always matches the JSON key (PascalCase in C#, same in JSON).