Chapter 02 · Configuration

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.

Start with what you're trying to do

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.
i
Everything here is harness infrastructure, not your app's config

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:

  1. appsettings.json — the default values, checked into source control. This is the file you'll spend the most time in.
  2. appsettings.Development.json — environment-specific overrides (only loaded when ASPNETCORE_ENVIRONMENT=Development, which is the default during dotnet run).
  3. User Secrets (dev) or environment variables / Azure Key Vault (prod) — for anything sensitive: API keys, connection strings, signing secrets. These override the JSON files.
Why three sources?

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:

C# · DI composition
services.Configure<AppConfig>(configuration.GetSection("AppConfig"));

From then on, any service can ask for an IOptionsMonitor<AppConfig>:

public class MyService(IOptionsMonitor<AppConfig> cfg) { private AppConfig Cfg => cfg.CurrentValue; public void DoThing() { var budget = Cfg.Agent.DefaultTokenBudget; // ... } }
Always inject 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.
!
Hot reload has limits

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:

AppConfig ├── Common // app name, version, slow threshold ├── Logging // log paths, pipe name, console suppression ├── Agent // request timeout, default token budget ├── Http // CORS, JWT auth, OpenAPI/Swagger ├── Infrastructure // FileSystem sandbox, state mgmt, content providers ├── Connectors // GitHub, Jira, Azure DevOps, Slack credentials ├── Observability // sampling, PII filter, exporters (Jaeger, Prom, AzMon) ├── AI // the big one — agents, MCP, RAG, governance, resilience │ ├── AgentFramework │ ├── AIFoundry │ ├── MCP // this app as MCP server │ ├── McpServers // external MCP servers this app consumes │ ├── A2A │ ├── ContextManagement │ ├── Permissions │ ├── Hooks │ ├── Orchestration │ ├── Skills // where SKILL.md files live │ ├── Agents // where AGENT.md files live │ ├── Rag │ ├── ModelRouting // model tiering (economy/standard/premium) │ ├── Embedding │ ├── ToolOutputCompression │ ├── Plugins // local plugin declarations │ ├── Egress // outbound SSRF allowlist │ ├── SandboxCapabilities // process/Docker sandbox capability model │ ├── WorkMemory // self-improving work memory │ ├── Audit // hash-chained tamper-evident audit │ ├── Resilience │ ├── DriftDetection │ ├── Learnings │ └── Governance // tool-invocation gate + DataClassification DLP ├── Azure // App Insights, SQL, B2C, Key Vault, Graph ├── Cache // Memory | Redis └── MetaHarness // optimization loop: iterations, eval tasks, traces

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.
When to raise the token budget

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.

json · appsettings.json
"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).
Don't widen this to "/"

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.

json
"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.)

json
"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)

bash
# 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):

json
"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

json
"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

json
"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):

json
"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)

json
"AppConfig": {
  "AI": {
    "Permissions": {
      "DefaultAutonomyLevel": "Autonomous"
    }
  }
}
!
Only safe in trusted environments

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

json
"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:

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

How to verify a config is bound correctly

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

src/Content/Domain/Domain.Common/Config/ ├── AppConfig.cs // root ├── AI/ │ ├── AIConfig.cs // AppConfig.AI │ ├── AgentFrameworkConfig.cs // .AgentFramework │ ├── SkillsConfig.cs / AgentsConfig.cs │ ├── MCP/ // McpServersConfig, server defs, auth │ ├── Permissions/ // PermissionsConfig, tier policies │ ├── Governance/ // EscalationConfig, threat levels │ ├── Resilience/ // fallback, circuit breaker, retry │ ├── Orchestration/ // SubagentConfig, streaming │ ├── RAG/ // ingestion, retrieval, reranker, query xform │ ├── ContextManagement/ // compaction, tool result storage, budget │ ├── Hooks/, DriftDetection/, Learnings/, WorkMemory/ │ ├── ModelRoutingConfig.cs, ToolOutputCompressionConfig.cs │ ├── PluginsConfig.cs, EgressConfig.cs, SandboxConfig.cs, AuditConfig.cs │ └── GovernanceConfig.cs // + DataClassification DLP ├── Azure/ // AppInsights, B2C, KeyVault, Graph, SQL ├── Cache/ // CacheConfig ├── Connectors/ // GitHub, Jira, AzDO, Slack ├── Http/ // CORS, auth, OpenAPI, policies ├── Infrastructure/ // FileSystem, content providers, state mgmt ├── MetaHarness/ // MetaHarnessConfig └── Observability/ // sampling, PII, exporters, pricing

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


Where to go from here