Approval policy and change tiers
Who may release what — the write floor, risk tiers L0–L3, the roles that decide, change classes, windows, the automatic lanes and every guard they check.
Reference · Updated 2026-10-28 · Describes release 0.1.164
This page is the one place that states who approves what. The how-to chapters describe the clicks and link here for the rules; when a rule on this page and a sentence elsewhere disagree, this page is the one checked against the code.
Related references: Changes reference (the change ledger, last-known-good records, rollback, harvest) and Compliance reference (controls, findings, risk decisions).
The write floor
Every call an agent makes that could change something pauses for a person. The gate is an allow-list of reads on top of default-deny: a call is free only when the classifier says it is a read; everything else waits for an approval. An incomplete rule costs one extra approval prompt, never a silent write. The credential that performs approved fixes (a write-role connection) is never visible to an agent at all.
| Call | Runs without approval | Pauses for approval |
|---|---|---|
API connector call (<name>_request, aws_request, azure_request, gcp_request, sentinel_query) |
Operations the read classifier lists for that connector type | Every other operation, including writes sent through a read path |
CLI connector call (<name>_exec) |
Read verbs; a connection's readonly_verbs may widen the read set for its own credential, never past the floor |
Any other verb |
Network device (net_exec) |
show / display reads |
Configuration pushes |
MCP server tools (<server>_<tool>) |
Tools the server annotates readOnlyHint: true, unless the tool name looks mutating |
Everything else (deny by default) |
http_get |
Never — it is the exfiltration channel | Always |
web_fetch |
Domains listed in settings['web_fetch_allowlist'] |
Any other domain (an estate x-ray scan denies it instead of pausing) |
web_search |
Always (the query is PII-scrubbed before it leaves) | Never |
lake_query |
Always — read-only by construction (single SELECT, no DDL, no file access) |
Never |
change_lookup, service_graph, read_attachment, read_evidence |
Always (read tools) | Never |
Two more rules sit above the floor:
| Rule | Where it is set | Effect |
|---|---|---|
| Extra approval gates | Agents page, per pillar (approval_required, a list of fnmatch patterns; default []) |
A matching tool call pauses even when the floor would let it through. Nothing can remove the floor. |
| Computer-assist mode | Guardrails, execution_mode: computer_assist |
The platform makes no write at all: no remediation, no rollback, no PR or ticket route. Plans are prepared for an operator to run outside the platform and report back. |
Diagnosis is the posture everywhere else: reads flow, so an analysis is never blocked on a human; an estate x-ray scan is read-only by construction and refuses a write instead of pausing (the refusal is counted on the scan as denied_writes).
The approval gate
When a gated call is reached the run stops at status paused_approval and an approval request is written with the tool name, the agent's one-line description, the parameters and the exact invocation shown to the approver. The run resumes when every pending request on it is decided.
| Fact | Value |
|---|---|
| Decisions | approved, denied, edited (approved with a human-modified command; a redacted command cannot be executed, the approver supplies a complete one) |
| Permission to decide | approvals.decide — POST /api/aiops/{workspace}/investigations/{id}/approvals |
| Tier on the request | Stamped when the request is created (see Risk tiers); approvals_required is the quorum for that tier |
| Quorum | approved_by counts distinct users: L3 needs two different people, a second click by the same person does not count. An earlier vote counts only while its member may still release the tier: a member removed, disabled or moved to a role without approvals.decide no longer counts, when the second approval lands or when the approved call is dispatched. A vote counts only for the command its member released: an edit is another command, so an edited decision with a different edited_command starts the count again on that command (it waits as the request's proposed edited_command), and a plain approval of the original after a proposed edit starts it again on the original. To complete an edit another approver proposed, send edited with the same edited_command |
| A denial | Goes back to the model as feedback; the run continues without the action |
| Timeout | aiops_approval_ttl_s = 86400 s (24 h). The scheduler denies an older pending request with the note "expired: no human decision within the approval TTL", audits approval.expired, marks the run failed ("approval expired") and hands the incident to a person (an incident already resolved is not reopened) |
| Audit actions | approval.requested, approval.approved, approval.denied, approval.edited, approval.released (a vote that did not complete the quorum), approval.dispatching, approval.executed, approval.receipt, approval.expired, approval.edit_rejected, execution.blocked |
A paused tool-call decision always needs approvals.decide. A prepared change has one narrow exception: an operator with incidents.act may release a non-emergency standard change made standard by a matched runbook at a one-approver tier (L1/L2). A P1 or critical incident and L3 still require their existing approvers and quorum. Setting a class by hand or choosing Standard by default does not grant this release permission.
Risk tiers
A tier decides who may release a proposal and how many of them. It never decides whether a human is involved — that is the floor above. Classification is pure: the same inputs always give the same tier, and no model takes part.
| Tier | On-screen definition | Policy label | Distinct approvers | Permission |
|---|---|---|---|---|
| L0 | Suggestion — no write path matched; a human applies it by hand | Suggestion — no write path; a human applies it | 1 | approvals.decide |
| L1 | Low risk — a reviewed pull request; the merge is the second pair of eyes | Low risk — reviewed merge or narrow blast radius | 1 | approvals.decide |
| L2 | Controlled change — touches live infrastructure or a production environment; needs an elevated approver | Controlled change — an approver releases it | 1 | approvals.decide |
| L3 | High risk — wide blast radius (3+ trusted dependents) or a device push; two distinct approvers | High risk — two distinct approvers required | 2 | approvals.decide |
How a tier is derived, in order; a later step may only raise the result:
| Step | Rule |
|---|---|
| 1. Base by channel | gitops_pr → L1 (the PR is inert until a reviewer merges it); api, mcp, cli → L2; network → L3; no matched write path → L0 |
| 2. Production label | Any alert label env, environment, stage or tier with value prod, production, prd or live → at least L2. Other values are not assumed safe, they are simply not escalated by environment |
| 3. Blast radius | 3 or more impacted services reached through a trusted dependency edge (declared or traced) → L3. Observed and discovered edges are shown to the human but never raise the tier |
| 4. Route override | A route's tier_override may raise the tier, never lower it |
| 5. Change class | A change request's class raises the release tier to its floor: standard L1, normal L2, emergency L3 (see Change classes) |
| 6. Several channels | A plan that spans channels is released at the tier of its riskiest channel |
Which channel a gated tool call is: net_exec → network, any other <name>_exec → cli, everything else → api (an unrecognised tool errs strict, as a controlled change). An unknown tier value is treated as L2.
Who decides: permissions and roles
| Action | Permission |
|---|---|
| Decide a paused agent action | approvals.decide |
| Approve, execute or roll back a change request; close a post-implementation review | approvals.decide |
| Run a legacy route from an incident (Remediate), record an executed fix | incidents.act or approvals.decide |
| Request a change, reclassify a change request, declare a change, ask for verification | incidents.act |
| Request a risk decision (fix or suppress) or a suppression on a control | incidents.act or approvals.decide |
| Approve, reject or revoke a risk decision | approvals.decide |
| Certify a runbook route (pre-approval) | approvals.decide |
| Pull changes or findings now, manage routes and scan settings | pillars.manage |
| Stop a run (kill switch per run), grant extra budget to an incident | incidents.act |
Builtin roles (synced from code at every start; custom roles may hold any subset):
| Role | Holds | Does not hold |
|---|---|---|
| owner | every permission | — |
| operator | everything except members.manage, pillars.manage, approvals.decide |
cannot approve, suppress or release on its own |
| approver | every *.view + approvals.decide, feedback.submit, chat.ask |
does not run analyses |
| viewer | every *.view |
— |
| executive | every *.view + chat.ask, approvals.decide |
runs nothing, signs off |
Two-person rules beyond L3: a risk decision is approved by someone other than its requester whenever another member holds approvals.decide (a sole approver may self-approve, audited self_approved; aiops_risk_no_self_approval: true forbids it outright); a route is certified by someone other than the person who last edited it whenever another approver exists; an L3 rollback needs a second distinct approver.
Change classes
Every escalated analysis mints one change request (status planned) carrying an ITIL class decided in code at wrap-up — never by the model.
| Class | Tier floor | Release rules |
|---|---|---|
| standard | L1 | A matched-runbook standard prepared fix may be released by one operator (incidents.act) or approver at L1/L2 — a certified runbook route's even at P1, with no emergency rules. L3 still needs two approvers. Certified step runbooks that match the fix exactly may also release it on their own where the workspace allows automatic fixes — an incident's fix only, never a compliance control, scanner finding, vulnerability or security alert (Step runbooks). |
| normal | L2 | One approver; a closed change window holds it |
| emergency | L3 | Two distinct approvers; once both have approved it passes a closed change window (audited change.window_bypassed); a post-implementation review is due from submission (Schedule the post-implementation review when an emergency change is submitted, on by default since 0.1.133; off, it opens when the write lands), due in aiops_pir_due_h hours (72) |
Approving and releasing are separate acts. The person who releases an approved change need not have approved it: an approver approves, and later anyone allowed to release that tier — the same approver, another approver or an owner — runs it. Running an approved change records no extra approval. At dispatch the platform checks again that the person releasing may still release the tier and that the approvals still valid meet the quorum; when either fails, nothing is executed and the audit log records change.release_refused with the reason. A quorum that is no longer met (an approval stopped counting after the release began, a scheduled or automatic release included) puts the change back to waiting for approval (change.pending), so the next approver's click runs it; any other refusal rejects the change. An approval whose approver may no longer release the tier (role changed, member removed, support access ended) stops counting before the click is taken: the person who presses Approve & run adds their own approval in its place, and the change runs once the quorum is met again.
Operator release is checked again at preview and dispatch: the operations must still be pre-approved and a route-based standard fix must still have the same enabled, certified runbook revision with its standard-class switch on. Revoking that basis removes operator release even though the recorded class remains historical evidence. The review digest binds the basis; a changed certification requires another review.
Under the derived policy (default): standard only when a certified runbook route (remediate-…) covers the fix — even at P1; emergency for any other P1 or critical incident; otherwise normal. A fetched skill alone never makes a change standard while Standard change for a runbook route's operations is on (the default); with it off, the older rule applies: standard when a runbook skill matched and every typed operation of the plan is on the workspace's pre-approved list (aiops_preapproved_operations) with no raw commands, file changes or non-restating manual commands beside them. Demoting, disabling or editing the runbook before the request is released makes it normal (emergency at P1). Under change_class_policy: standard_default ("Standard by default" on Guardrails) every request starts standard and people raise it. Under both, a class a person set on an earlier request with the same operations is inherited. Anyone who may request a change may only raise the class (incidents.act): normal to emergency, or a standard no runbook backs; a runbook's standard moves only with its runbook. A new class clears the votes. A class only ever raises the tier. The exact derivation, the runbook-route exception and the reclassify rules are in the Changes reference.
Change and freeze windows
| Window | Where | Meaning |
|---|---|---|
| Change window | params.change_window on a route; settings['change_windows'] first matching service glob, then settings['change_window'] for prepared plans |
When a human may release an L2/L3 change: {"days": ["mon", ...], "hours": "HH:MM-HH:MM"}, UTC. Days alone = those whole days; hours alone = every day in those hours; an overnight range wraps midnight; no window = always open; a malformed window fails open (a typo must never lock remediation out) |
| One-time exception | meta.window_exception on one change, requested on the change page and approved by approvals.decide |
Opens only that digest-bound change during its approved UTC interval; it is a separate approval, never a release vote, and it does not change class, tier or quorum |
| Freeze window | settings['change_freeze'] (same shape) |
When an external change with no in-flight platform change for its service is flagged authorized: false and audited change.unauthorized_detected |
Who checks the change window: the human release of a legacy route at L2/L3 (403 outside it), every human release of a live-infrastructure change (api, cli, mcp, network at L2/L3 — checked at the request, at dispatch and at each step), and the direct API lane before it releases. A pull request never checks it (the auto path may open a PR at 3 am; it is inert until merged), and neither does the rollback sweep (restoring the last-known-good state is the remedy for a change that already went wrong). An approved one-time window is the separate exception for one change; a genuine emergency still uses its full quorum and existing bypass audit/PIR semantics.
Automatic lanes and their guards
Four things can happen without a per-incident click. Each lane re-checks every guard at fire time; the first failing guard is the answer, and a pre-approved route that matched but did not run leaves one route.match_skipped row on the incident with the reason ("Pre-approved route matched — not run" on the incident page).
Pre-approved GitOps runbooks
Fires where the human Remediate click would, after an analysis finished escalated with a matched route.
| Guard | Skip reason when it fails |
|---|---|
| Not an unauthorized change or a denied-access report (a person decides, whatever runbook is certified) | unauthorized_change |
| Not a compliance control, scanner finding, vulnerability or security alert (a finding is fixed only after a person approves it, never by a route or a step runbook on its own) | finding_lane |
settings['aiops_auto_remediation'] on |
auto_fix_off |
| Not computer-assist mode | execution_disabled |
| Kill switch off, daily and monthly limits not reached, licence allows runs | budget |
| Never fired before on this incident (one shot) | one_shot |
| The incident still escalated, not verify-muted, not a Quick-start test alert | (silent) |
| A route selected for this incident, enabled and certified at its current revision | uncertified_revision, not_preapproved, demoted, analysis_named_other, other_route_selected, typed_steps_won, route_not_offered, … |
| Tier at most L2 | tier_l3 |
Mode gitops_pr; any other mode goes through the direct API lane below |
the direct lane's own code |
| Not unmapped, not flapping, the route's recorded fix on this key not "not a good state" | audited incident.auto_remediate_blocked |
The run is audited incident.auto_remediate with the route revision, who certified it and the precedent change. The change window is not consulted for a pull request.
Direct API fixes
Off by default. settings['aiops_auto_remediation_direct'] under the master switch above lets a certified API runbook make its one call without a click.
| Guard | Refusal code |
|---|---|
| Not an unauthorized change or a denied-access report | unauthorized_change |
| The route may run on its own (API mode, both switches on) | never_alone reasons |
| Certified at its current revision | uncertified_revision |
| Certified by a named person (not a migrated pre-approval) | certifier_unknown |
That person still holds approvals.decide |
certifier_lapsed |
A runbook route: enabled, named remediate-…, selector set, not an on_open / on_resolve ticket route |
not_a_runbook |
params.operations lists exactly the typed operations, every one on aiops_preapproved_operations, none a revert operation, all for the connection's cloud (aws, azure, gcp) |
operations_not_preapproved |
| Exactly one operation and the configured call is that operation's compiled form | call_not_bound |
No ${…} placeholder anywhere in the route |
templated_action |
params.snapshot declared (before-state) |
no_before_state |
params.rollback declared |
no_rollback |
| The selector matches the incident's opening alert | pinned_labels |
| No other API runbook matches that alert | ambiguous |
| Not P1 and not critical (an emergency is never automatic) | emergency_class |
| The latest analysis did not ask for a person | analysis_asked_for_person |
| Not a reopened incident; this route has not already run on it | reopened, ran_here |
| Inside the route's change window | the window refusal |
| No in-flight change on the route | route_busy |
| A verified precedent: the same fix, released by a person and validated, on the same alert | no_precedent |
After the write the post-execution validation runs; a failed validation follows the rollback rules below and, with aiops_demote_on_rollback on, the first rollback takes the route's pre-approval away.
Automatic rollback
The scheduler sweeps changes that landed failed in the last aiops_rollback_sweep_window_h hours (24), once per change:
| Failed change | Decision |
|---|---|
| Pull request whose post-fix verification failed, tier ≤ L2 | Restore PR opened (inert until merged) |
| Pull request whose execution failed before a PR existed | Skip — nothing went live |
| Any change the provider refused before acting | Skip |
| A prepared plan that stopped at its backup (no write step ran, and no person reconciled the write as executed) | Skip — nothing was applied |
Verification uncertain on a live change |
Wait for a real verdict |
Operator-declared rollback script, settings['aiops_auto_rollback'] on, tier ≤ L2 |
Executed |
| Compiled inverse of a posture step | Escalate — unless aiops_auto_rollback_cloud (L1 or L2) covers the tier and the post-change metric window failed |
| Forward action was itself a recovery (start / restart / reboot / reset / redeploy) | Escalate — its inverse re-creates the incident |
| L3, manual, external, computer-assist, already attempted | Escalate / skip / wait |
| Any change of an unauthorized-change incident (a revert an approver released, a fix someone ran) | Escalate — a person rolls it back |
An agent-drafted rollback is never auto-executed. Full matrix: Changes reference → Rollback paths.
Risk decisions and compliance fixes
Never automatic. A risk decision lands as a request (requested) and takes effect only when an approver approves it; a compliance control's fix is a change request through the change hub — compliance holds no write path of its own. Suppressing an accepted finding at the scanner (Security Hub SUPPRESSED, an Azure Policy exemption) is the one compliance-side cloud write, opt-in per workspace (aiops_risk_suppress_at_source), metadata only, through the write-role connection of the same account. See the Compliance reference.
Kill switch, budgets and licence
Every point that would start new model spend asks the same guard; signals are never dropped — a blocked alert-driven run is marked failed and the incident is handed to a person.
| Control | Setting / action | Effect |
|---|---|---|
| Pause all agents | settings['agents_paused'] (Guardrails) |
No new run, no automatic fix, no automatic rollback; alerts and incidents keep recording |
| Daily limit | settings['daily_cost_limit_usd'] (0 = none; platform default aiops_daily_cost_limit_usd_default) |
Trips at UTC midnight boundaries |
| Monthly limit | settings['monthly_cost_limit_usd'] |
Trips from the 1st, UTC |
| Per-incident limit | settings['incident_cost_limit_usd']; POST …/incidents/{id}/budget-grant (incidents.act) |
Over the cap the incident escalates; a person grants more |
| Per-run cap | aiops_max_cost_usd_default / tenant cost cap |
Best-of-2 doubles it for the second pass |
| Stop one run | POST …/investigations/{id}/cancel (incidents.act) |
A remote runner gets the stop at its next checkpoint |
| Licence | expired past grace or invalid | New runs and approved releases pause, like the kill switch |
| Computer-assist | execution_mode: computer_assist |
No platform write of any kind |
Benchmark and test-lab runs are excluded from every spend total (test spend is not operating cost).
The Needs you queue
GET /api/aiops/{workspace}/needs-you lists every decision waiting on a person, most urgent first; each source is shown only to members holding its permission.
| Source | What it is |
|---|---|
incident_approval |
A paused agent action |
incident_escalated |
An incident handed to a person |
change_approval |
A change request awaiting its quorum |
merge_proposal |
An advisory incident merge |
risk_request |
A requested risk decision or control exception |
pir |
An open post-implementation review (overdue past its due date) |
unmapped_signal |
An alert that named no service |
problem_proposal |
A recurring problem's automation candidate |
route_demoted |
A runbook route that lost its pre-approval after a rollback, a failed validation or a fix that did not hold |
route_certification |
A route revision waiting to be certified |
route_skipped |
A pre-approved route that matched but did not run for a reason a route edit or a setting would fix |
Where this is explained in context
Chapters that mention tiers, approvers or the gate today; each should link here rather than restate the rules.
- Approve and run a fix — the gate on the incident page, the Needs you page, permissions at a glance
- Change windows, emergency changes and rollback — the procedure for windows, the emergency release and rollback
- Runbooks and automatic fixes — certification, the switches, why a route did not run
- Automatic API fixes — the direct lane's guards in procedure form
- Write connections and approved fixes — consent, the first release
- Read and write roles, and the write gate — the read/write gate per connector type
- Configure the agents — extra gates per pillar
- Guardrails and cost limits — cost limits, the class policy and the switches around a fix
- Security and data handling — the write gate as a control matrix
- Permissions — the full permission matrix
- Security posture — requesting and approving risk decisions
- Turn on compliance — compliance fixes as change requests
- MCP operation reference and REST API — the API and MCP operations that decide and release
- Glossary — tier, class and gate terms