Chapter 03 · Access Control

Autonomy & Governance

The previous page settled who is allowed in. This page is layer 2: how much an agent may do on its own, and the human-approval machinery for when it wants to do more. Identity proves the caller is real; autonomy decides how far that real caller is trusted to act unattended — and governance is the gate it has to pass to act at all.

Autonomy is a dial, not a switch

The tempting mental model is binary: an agent is either "supervised" (a human approves everything) or "let loose" (it does whatever it wants). That model is wrong, and it is dangerous, because it makes "give it more freedom" feel like flipping off the safety system.

The harness models autonomy as a tier — a setting that establishes the default posture for an agent's actions. A higher tier changes the default from "ask a human first" to "go ahead", but it never removes the hard limits underneath. Think of it as a dial that controls how much the agent does without prompting you, sitting on top of a floor of safety rules the dial cannot reach.

The three autonomy tiers

The tier is a three-value enum defined in src/Content/Domain/Domain.AI/Governance/AutonomyLevel.cs. Each value sets the default permission posture for the agent: whether a consequential action defaults to Ask (pause for a human) or Allow (proceed).

Tier Value Posture Default behavior
Restricted 0 Read-only Default Ask — every consequential action needs approval.
Supervised 1 Recommend-and-wait Default Ask, but specific tools can be granted explicit Allow overrides.
Autonomous 2 Acts within guardrails Default Allow — but safety gates and Deny rules remain a hard ceiling it cannot exceed.
!
"Autonomous" is not "unrestricted"

This is the point engineers most often get wrong. Setting an agent to Autonomous flips the default from Ask to Allow — it does not disable the safety floor. The governance behavior, approval workflows, and every explicit Deny rule still apply. An Autonomous agent that tries a denied action is still blocked. The tier raises the floor of convenience; it never lowers the ceiling of safety.

How a tier gets assigned

An agent doesn't pick its own tier. It is assigned by src/Content/Application/Application.Core/Permissions/AutonomyTierRuleProvider.cs, which reads the tier from one of two sources:

  • Configuration — a TierPolicies section that maps agents to tiers explicitly.
  • The agent's SubagentType — its declared role. An IAutonomyTierResolver maps each SubagentType to an AutonomyLevel, so a "read-only researcher" subagent and a "deployment" subagent can carry different default postures without per-agent config.

From that tier, the provider emits permission rules at two priorities:

Priority 0 — the baseline rule
One rule expressing the tier's default posture (Ask or Allow). It is the lowest priority, so it is the fallback that applies when nothing more specific matches.
Priority 10 — per-tool override rules
Higher-priority rules for specific tools — for example, a Supervised agent granted an explicit Allow on one safe tool. Because they outrank the baseline, they win where they apply.
i
These rules feed the permission resolver

The Priority 0 and Priority 10 rules produced here are not the final word — they are inputs to the per-tool permission resolver covered on the next page. Autonomy decides the default and the broad overrides; the resolver merges those with tool-level allow/deny lists to reach the concrete decision. See Tools & Permissions.

i
Running an agent you did not write

Everything on this page assumes the agent's manifest is one you shipped. The harness also has a surface that accepts an agent uploaded over HTTP — the Bundle API. There, the uploaded manifest is treated as untrusted input: it may only request tools and an autonomy tier, and a host-side capability envelope, keyed to the caller's credential, intersects that request down to what the caller is actually allowed. The autonomy that ends up enforced is the most restrictive of three things: the envelope's ceiling, the host's own graded-autonomy gate, and the individual tool's blast radius. It can only tighten, never loosen. See Onboarding · Ch. 17 Execution API.

The governance gates, and why they fail closed

A tier sets defaults, but the concrete gating happens on the live tool-execution path, not the MediatR request pipeline. Every agent tool is wrapped by src/Content/Application/Application.AI.Common/Services/Tools/GovernedAIFunction.cs, whose InvokeCoreAsync runs three ambient gates in order before the tool executes, then optionally scrubs the result:

  1. IToolInvocationGovernor — authorization (Services/Governance/ToolInvocationGovernor.cs). Opt-in via GovernanceConfig.EnforceToolInvocation. It chains the per-tool permission resolver, a graded-autonomy risk gate, capability enforcement, and the YAML policy engine (rate limits, approval workflows). If a policy says the action requires approval, the governor records a PendingApproval and blocks fail-closed — live mid-tool-call escalation is deliberately deferred, so it does not route to IEscalationService in the middle of a tool call.
  2. IToolClassificationGate — Microsoft Purview data-classification DLP. Opt-in via AppConfig:AI:Governance:DataClassification:Mode (Off/Audit/Enforce). A Block verdict returns a model-facing message instead of running the tool; a RedactOutput verdict lets the tool run, then scrubs the result. See Data Protection.
  3. IProgressEvaluator — the spin / no-progress guard. It halts a tool call when the agent is repeating identical calls without making progress.

All three gates are inert unless enabled — the classification gate defaults to Off, and the governor and progress guard are gated on their config flags. Two earlier MediatR behaviors named GovernancePolicyBehavior and ToolPermissionBehavior were removed as dead code: they keyed on a request marker that nothing in production implemented, so they never actually fired. Governance now lives on the tool path described above.

Fail closed: the safe default is "no"

The governor fails closed. If evaluation cannot affirmatively approve an action — because a policy is missing, ambiguous, or errors out — the answer is deny (recorded as a PendingApproval), never allow. A bug or a gap in your policy file makes the agent more restricted, not less. This is the opposite of a fail-open system, where an error would silently let the action through.

Human-in-the-loop: escalation strategies

When a policy demands approval, the request becomes an escalation — a pause where one or more humans must decide. How their votes resolve is set by an approval strategy. The harness ships three, in src/Content/Application/Application.Core/Escalation/Strategies/:

AllOf — unanimous
AllOfApprovalStrategy.cs. Every approver must approve. A single denial immediately resolves the whole request as denied — there is no point waiting for the rest once one person has said no.
AnyOf — first response wins
AnyOfApprovalStrategy.cs. The first response — approval or denial — resolves it. Fast, for low-stakes actions where any one trusted reviewer suffices.
Quorum — N-of-M threshold
QuorumApprovalStrategy.cs. Approval needs N of M approvers (e.g. 2 of 3). Used when one person isn't enough but you don't want to require everyone.
!
Quorum fails closed on a misconfigured threshold

The headline safety property: if the configured quorum threshold is ≤ 0 — a misconfiguration that, naively coded, would mean "zero approvals needed, auto-approve everything" — QuorumApprovalStrategy returns denied instead. A broken quorum config can never become an open door. The dangerous failure mode (a typo silently disabling approval) is engineered out.

The escalation itself is described by src/Content/Domain/Domain.AI/Escalation/EscalationRequest.cs, which carries the ApprovalStrategyType (which of the three above), the list of approvers, the quorum threshold, and a timeout action.

A timed-out approval does not auto-approve

The default timeout action is DenyAndEscalate. If approvers never respond, the request is denied and escalated further — it does not quietly approve itself because the clock ran out. Silence is treated as "no", consistent with the fail-closed posture everywhere else on this page.

Tamper-evident audit

Every governance decision is logged so that "the agent did X, and it was approved by Y" is provable after the fact. The logging path is src/Content/Infrastructure/Infrastructure.AI.Governance/Adapters/AgtAuditAdapter.cs, which wraps the AgentGovernance Toolkit (AGT) AuditLogger behind the harness's IGovernanceAuditService interface.

Log(agentId, action, decision) writes a structured JSONL entry — one JSON object per line — recording who acted, what they did, and how it was decided. The integrity guarantee comes from VerifyChainIntegrity(), which delegates to AGT's tamper-evident hash-chain verification.

Hash chain

A log structured so each entry includes a cryptographic hash (a short fixed-length fingerprint) of the previous entry. Because every entry is fingerprinted into the next, you cannot delete, reorder, or edit an old entry without breaking the fingerprint of every entry after it — and the break is detectable. It is the same idea that makes a blockchain tamper-evident: not that the log can't be altered, but that any alteration is provably visible. That is what VerifyChainIntegrity() checks.

Attack scenario: the destructive action with no approvals

Slipping a destructive action past the humans

Suppose an attacker — via an indirect prompt injection — coaxes an agent into requesting a destructive tool call (delete a dataset, push to production). The governance policy requires a 2-of-3 Quorum approval for that action. The attacker's angle is to make the approval requirement evaporate: trigger the action at a moment when the approver list is empty, or exploit a config where the quorum threshold was fat-fingered to 0.

Both fail. The IToolInvocationGovernor fails closed, so a request it cannot affirmatively approve is denied. QuorumApprovalStrategy returns denied on a threshold of ≤ 0 rather than auto-approving. And if approvers are simply unreachable, the DenyAndEscalate timeout action denies the request instead of waiting it out into a yes. The destructive action never runs — and the attempt is written into the tamper-evident audit chain for the investigation that follows.

Where this layer hands off

Autonomy and governance decide whether an action is allowed or needs approval. They do not, by themselves, resolve the fine-grained per-tool allow/deny decision, nor do they inspect what the agent says back. Those are the next two stops:

  • The Priority 0 / Priority 10 rules from this page feed the per-tool permission resolver on Tools & Permissions — that page covers how allow/deny lists combine into the final decision for a specific tool.
  • Separately, a ResponseSanitizationBehavior inspects the agent's output for leaked secrets and injection payloads before it leaves the system. That belongs to Content Safety & Injection — covered there, not duplicated here.