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.
|
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
TierPoliciessection that maps agents to tiers explicitly. -
The agent's
SubagentType— its declared role. AnIAutonomyTierResolvermaps eachSubagentTypeto anAutonomyLevel, 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:
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.
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:
-
IToolInvocationGovernor— authorization (Services/Governance/ToolInvocationGovernor.cs). Opt-in viaGovernanceConfig.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 aPendingApprovaland blocks fail-closed — live mid-tool-call escalation is deliberately deferred, so it does not route toIEscalationServicein the middle of a tool call. -
IToolClassificationGate— Microsoft Purview data-classification DLP. Opt-in viaAppConfig: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. -
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.
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/:
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.
AnyOfApprovalStrategy.cs. The first response — approval or denial —
resolves it. Fast, for low-stakes actions where any one trusted reviewer suffices.
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.
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.
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.
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
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
ResponseSanitizationBehaviorinspects 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.