Declined Means Stop: An AI Agent Consent State Machine and Fail-Closed Test Matrix

Treat every AI agent consent state except granted as a hard stop. One state machine for banners, GPC, MCP elicitation and OAuth, plus a release test matrix.

Hero illustration for AI agent consent: the title Declined means stop beside six consent state chips, where only granted lets an action run and declined, cancelled, revoked, expired and unknown all stopHero illustration for AI agent consent: the title Declined means stop beside six consent state chips, where only granted lets an action run and declined, cancelled, revoked, expired and unknown all stop
Six consent states, one exit. Only granted lets the action run.

The Model Context Protocol gives a user three answers to an elicitation prompt, and the example handling it gives for the second one, decline, is to offer alternatives. That is sensible advice for a form. For an agent holding a shell, a browser and a dozen MCP servers, it is also how a “no” turns into the same outcome reached through a different tool.

This playbook makes AI agent consent fail closed. You get one state machine for site, ad and agent actions, with six states (granted, declined, cancelled, revoked, expired, unknown) and one rule: anything but granted is a typed error and a stop. You also get a test matrix that runs on every release and every consent-tool or tag change, so the rule is proven rather than assumed.

The stop-on-decline rule is deliberately stricter than the MCP guidance, and we say so wherever it matters. This is an engineering test, not legal advice; the rule text and regulators are cited so your counsel can set the legal floor and your tests can sit above it.

The MCP specification, version 2026-07-28, states the principle twice. “Users must explicitly consent to and understand all data access and operations,” and “Hosts must obtain explicit user consent before invoking any tool.” It also names its limit: “MCP itself cannot enforce these security principles at the protocol level.” Enforcement lives in the host you ship.

The elicitation chapter splits refusal in two. Decline means “User explicitly declined”; cancel means “User dismissed without making an explicit choice”, which covers a closed dialog, a click outside, Escape or a browser that failed to load. Servers “MUST handle cases where the user declines or cancels”, and the suggested handling is “offer alternatives” on decline and “prompt again later” on cancel. Payment and credential flows must use URL mode.

MCP specification elicitation page listing the three response actions, accept, decline and cancel, with decline defined as user explicitly declined the request and cancel as dismissed without making an explicit choice Screenshot: Model Context Protocol, “Elicitation - Model Context Protocol” (specification version 2026-07-28), captured Oct 5, 2026.

California keeps tightening the web side. At its Aug 6, 2026 board session on opt-out preference signals, the California Privacy Protection Agency set out that on a signal, businesses must “stop selling and sharing any personal information associated with that browser or device”, and that the Opt Me Out Act requires browsers to offer such a signal from Jan 1, 2027. Enforcement this year punished half-honored opt-outs: per Potomac Law’s summary, the Attorney General’s $2.75M action against Disney (February) turned on a GPC signal honored on one device only and ad-tech pixels that kept firing after a webform opt-out, and a $1.1M PlayOn Sports action followed in March.

One scope correction: California’s updated CCPA regulations add automated decision-making technology duties “If a business uses ADMT to make significant decisions about a consumer”, with compliance from Jan 1, 2027. Significant decisions mean things like jobs, lending and housing. A coding agent refactoring your repo is not that, so don’t let ADMT stand in for the consent work below.

The EU moved dates, not consent. The Digital Omnibus on AI, in force since Jul 27, 2026, pushed Annex III high-risk obligations to Dec 2, 2027 and Annex I to Aug 2, 2028; Article 50 already applies (see EU AI Act Article 50 is live). GDPR consent is unchanged. The separate cookie package is still a proposal: Osborne Clarke describes a single-click refusal with a six-month no-re-ask (draft Art. 88a), and Nixon Digital reported in August that the Council dropped the browser-signal article (88b).

Google consolidated its controls. Per Google Analytics Help, “starting June 15, 2026, Google Analytics will transition to using Consent Mode (within Google Ads) as the single control for data”. An ads-personalization change is promised “later in 2026”, with dates to be “shared later this year”; none had been published by Oct 5.

Google Analytics Help announcement titled Updates to Google Analytics Data Controls, describing a plan to consolidate controls so Google Ads settings govern Ads data and Analytics settings govern reporting data Screenshot: Google Analytics Help, “Updates to Google Analytics Data Controls - Analytics Help” (June 15, 2026 change), captured Oct 5, 2026.

Why agents make “offer alternatives” dangerous

A web form that hears “no” can show a different form. An agent that hears “no” has other tools: if the GitHub MCP server’s write is declined, the shell can still run git push, and the browser can still click the merge button. Offering alternatives, read literally by a planner, is a route around the human.

Cancel is the quieter trap. A dismissed dialog, an Escape key and a failed page load all produce the same cancel, so the agent cannot tell “not now” from “never saw it”, and generic retry middleware turns it into a nag loop. Revocation must reach work already in flight. Decide the fail mode before the outage, the discipline of writing a gate’s fail mode before it returns 429.

Six steps. The first three define and wire the machine; the last three test it and keep testing it.

Step 1: Define six states and one rule

Every consent source in your product resolves to exactly one of these states at the moment of an action.

State Means Typical sources What the system does Path back to granted
granted Explicit yes, in scope, not expired Banner accept, elicitation accept, OAuth grant, OS permission allowed Action runs within the granted scope n/a
declined Explicit no “Essentials only”, elicitation decline, OS permission denied, opt-out signal Typed error, stop, no alternative path to the same outcome Only a new grant the user starts
cancelled Dismissed without a choice Elicitation cancel: closed dialog, Escape, load failure Typed error, stop the current action One re-ask, at a human checkpoint
revoked Was granted, then withdrawn OAuth revocation, banner changed, telemetry turned off Stop in-flight work at the next tool boundary, purge queued sends Only a new grant the user starts
expired Grant past its lifetime Token expiry, consent record older than your re-ask period Typed error, stop Re-ask at a human checkpoint
unknown No usable record Consent service down, banner failed to load, unparseable record Typed error, stop; never default to granted Once a valid record exists

The rule: the action proceeds only in granted. Every other state raises the same error type and stops. Here is where the rule departs from MCP on purpose. The spec suggests offering alternatives on decline; we forbid any alternative that reaches the same outcome. The spec suggests prompting again later on cancel; we allow one re-ask, at a checkpoint a human is already looking at, never from a retry loop.

State machine diagram for AI agent consent: unknown can become granted by explicit accept; granted can become revoked or expired; declined, cancelled, revoked, expired and unknown all lead to a stop with a typed error, a halted session, no silent retry and an audit lineState machine diagram for AI agent consent: unknown can become granted by explicit accept; granted can become revoked or expired; declined, cancelled, revoked, expired and unknown all lead to a stop with a typed error, a halted session, no silent retry and an audit line Five of six states end in the same stop. Only an explicit accept reaches granted.

Give the stop one error shape everywhere, so a tag manager, a server sender and an agent host can all log and test it the same way. An illustrative shape:

{
  "error": {
    "type": "consent_not_granted",
    "state": "declined",
    "source": "mcp_elicitation",
    "scope": "repo.write:payments-service",
    "blocked_action": "create_pull_request",
    "retryable": false,
    "halt_session": true,
    "audit_id": "cns_20261005_0142"
  }
}

Write this table into the repo next to the code that reads each signal. A source with no row is a source nobody tests.

Source Signal you read State Where it’s enforced
Cookie banner “Essentials only” Consent record with ad and analytics categories off declined, per category Tag manager and server sender
Global Privacy Control Sec-GPC: 1 request header declined for sale and sharing Edge or server middleware, per request
Google Consent Mode ad_storage, ad_user_data, ad_personalization denied declined for ads Tag manager, and mirrored server-side
MCP elicitation decline action: "decline" declined Agent host
MCP elicitation cancel action: "cancel" cancelled Agent host
OAuth grant revoked Revocation event, or refresh fails with invalid_grant revoked Agent host and token broker
OS permission denied Screen, files, accessibility or camera prompt answered no declined Desktop app
Telemetry opt-out In-app setting turned off revoked if it was on, else declined App and telemetry backend
Consent store unreachable Timeout or parse error unknown Everywhere

Two notes. GPC arrives per request and per browser, so honor it where the request lands and propagate it to the account if the user is signed in; the Disney case turned on a signal honored on one device only. And server-side ad events need the same state as the tags: a Pixel revoke does not stop a server sender. OpenAI’s Ads Conversions API docs put the rule plainly, “If the user revokes consent, stop sending it”, and the install-attribution ledger enforces it with a gate in front of every send.

Step 3: Make the stop real at each layer

A consent rule that lives in one layer fails open in the others. Wire the same state into all five.

  1. Tags. Default every non-essential tag to denied. Nothing fires until a granted record exists for its category.
  2. Server senders. Read the same consent store as the tags. No record or a non-granted record means no event, logged with a reason.
  3. Agent tool calls. The host checks consent before invoking any tool, because the protocol cannot. A non-granted state returns the typed error instead of calling the tool.
  4. Sessions. On decline or revocation mid-session, halt at the next tool boundary and surface the session to a human. Interrupt semantics matter here: stop at a boundary, don’t orphan half-applied work.
  5. Retry policy. Mark consent_not_granted non-retryable in every retry wrapper, and forbid the planner from choosing another tool for the same blocked outcome.

An illustrative host policy:

consent_policy:                     # illustrative host policy, not a spec
  default_state: unknown
  proceed_only_when: granted
  on_not_granted:
    error_type: consent_not_granted
    retryable: false
    halt_session: true
    surface_to_human: true
    alternative_tools_for_same_outcome: forbidden
  per_state:
    cancelled: { reask: next_human_checkpoint, max_reasks: 1 }
    expired:   { reask: next_human_checkpoint, max_reasks: 1 }
    revoked:   { stop_at_next_tool_boundary: true, purge_queued_sends: true }
  audit:
    sink: append_only_log
    fields: [ts, state, source, scope, blocked_action, session_id, actor]

The “same outcome” clause is the one teams skip. Define outcomes, not tools: “push to the payments repo” is blocked whether the attempt comes from an MCP server, a shell command or a browser click.

Step 4: Build the test matrix

Rows are consent sources; columns are the behaviour each must produce. “Must” is a pass condition; “n/a” means the layer isn’t involved for that source.

Source (state) No tag fires No server-side event Tool call aborted with typed error Session halts, human sees it No silent retry Audit line written
Banner “Essentials only” (declined) Must Must n/a n/a Must: no re-prompt on the next page Must
GPC header (declined) Must, sale and share tags Must n/a n/a Must Must
MCP elicitation decline (declined) n/a Must Must Must Must: no alternative tool Must
MCP elicitation cancel (cancelled) n/a Must Must Must Must: at most one re-ask at a checkpoint Must
OAuth grant revoked (revoked) n/a Must Must Must Must: no re-auth loop Must
OS permission denied (declined) n/a n/a Must Must Must Must
Telemetry opt-out (revoked) Must Must n/a n/a Must Must
Consent store down (unknown) Must Must Must Must Must Must

How to drive each row in CI:

  • Banner and Consent Mode. A headless browser clicks “Essentials only”, loads three pages and asserts that the network log holds no ad or analytics requests and that the server sender logged zero sends for the session.
  • GPC. The same browser run with Sec-GPC: 1 set, signed in and signed out. Then sign in on a second browser without the header and assert the account-level opt-out still holds.
  • Elicitation. A test MCP server that answers every elicitation with decline, then with cancel. Assert the typed error, the halted session, no second tool touching the same outcome, and at most one re-ask for cancel.
  • OAuth revocation. Revoke the grant through the provider’s API in the middle of a scripted session. Assert the next tool call fails closed and nothing queued is sent afterwards.
  • OS permission. A manual row on a test machine per OS release; record the build and the result.
  • Telemetry opt-out. Toggle off, generate activity, and assert the telemetry backend received nothing after the toggle timestamp.
  • Unknown. Point the consent client at a dead endpoint and assert every layer stops.

The elicitation row, as an illustrative test against a fake MCP server:

def test_elicitation_decline_stops_the_session(host, fake_mcp):
    # illustrative: the fake server declines every elicitation
    fake_mcp.answer_elicitations_with("decline")
    result = host.run_task("open a pull request on payments-service")
    assert result.error.type == "consent_not_granted"
    assert result.error.state == "declined"
    assert result.error.retryable is False
    assert result.session.halted and result.session.surfaced_to_human
    assert not host.tool_log.touched("payments-service", after=result.error.ts)
    assert host.audit_log.has(result.error.audit_id)

Copy it for cancel and change two assertions: the state, and a check that at most one re-ask reached a human checkpoint.

Run the matrix on change, and add dated reruns for the days the outside floor moves.

Trigger What to rerun Owner
Every release Full matrix Release manager
Consent platform, banner or tag change Banner, GPC, Consent Mode and telemetry rows Web owner
New MCP server, tool or OAuth scope Elicitation, OAuth and unknown rows Agent platform owner
Agent host or SDK upgrade Full matrix Agent platform owner
New OS release on a supported desktop OS permission row Desktop owner
Jan 1, 2027 (Opt Me Out Act, CCPA ADMT compliance) GPC row, against counsel’s updated floor Privacy owner
Google publishes the ads-personalization date Consent Mode row Web owner

Timeline chart of AI agent consent dates from 2026 to 2028: the Disney opt-out action, Google Analytics Consent Mode change, EU Digital Omnibus on AI in force, MCP specification 2026-07-28, CPPA opt-out signal session, Opt Me Out Act and CCPA ADMT compliance, and EU AI Act high-risk datesTimeline chart of AI agent consent dates from 2026 to 2028: the Disney opt-out action, Google Analytics Consent Mode change, EU Digital Omnibus on AI in force, MCP specification 2026-07-28, CPPA opt-out signal session, Opt Me Out Act and CCPA ADMT compliance, and EU AI Act high-risk dates Dates that move the floor under your consent tests. Spacing is not to scale.

Each pass needs a time to stop, not only a yes. Set yours per layer: tags before the next page load, server senders before the next batch, agent actions before the next tool call. California’s regulations (11 CCR § 7026(f)) require a business to stop selling and sharing “as soon as feasibly possible, but no later than 15 business days from the date the business receives the request”; treat that as the legal ceiling, not your target.

Step 6: Read the matrix as a release gate

  • Any “Must” cell that fails blocks the release. No waivers for consent rows.
  • Store results with the build ID, the consent-platform version and the MCP spec version you tested against.
  • Diff the source table in Step 2 against last release. A new source with no matrix row fails the gate.
  • Review the audit log weekly: blocked actions and audit lines should match one to one.
  • Six states defined; one error type; granted is the only state that proceeds.
  • Every consent source mapped to a state in the repo.
  • Tags default to denied; server senders read the same store.
  • Agent host checks consent before every tool call.
  • “Same outcome” blocked across tools, not only the declined tool.
  • consent_not_granted non-retryable everywhere; one re-ask max, at a checkpoint.
  • Revocation reaches in-flight sessions and purges queued sends.
  • Matrix runs in CI on release and on consent-tool changes, plus dated reruns.

Unknown treated as granted. The banner script fails to load and the tag manager’s default is “fire”. Signal: ad requests in sessions that have no consent record at all.

One device, one browser. GPC is honored in the browser that sent it while the signed-in account keeps sharing elsewhere, the pattern in the Disney case. Signal: opt-out rates that differ sharply by device for the same accounts.

Server side never heard. The Pixel pauses on revoke and the server sender doesn’t. Signal: server events exceed consent-granted records for the period.

Decline routed around. The planner treats “offer alternatives” as an instruction and reaches the same outcome with the shell. Signal: after a decline, the same repo, file or account touched by a different tool in the same session.

Cancel as a nag loop. Retry middleware re-asks every few seconds. Signal: more than one prompt for the same scope within minutes; rising accept rates right after repeated prompts, which is the approval fatigue pattern arriving through consent.

Revocation that waits for the next session. The token is revoked but the running session holds a cached client. Signal: tool calls under a revoked scope after the revocation timestamp.

Silent drops with no audit. The stop works but leaves no trace, so nobody can prove it. Signal: blocked-action counts that don’t match audit-line counts.

A fleet with ten agents and three consent-handling styles has no consent policy; it has ten. The same argument that made permission modes fleet policy applies here: define the states once, enforce them in every host, and audit them across the fleet rather than agent by agent. The matrix is how you prove the policy holds on a Tuesday, not only on the day it was written.

The machine also carries into money and measurement. A checkout mandate is a granted state with a cap and an expiry, which is how the pre-authorization checklist for checkout agents treats it, and an ad event is only sendable from a granted record, which is the gate in the install ledger. Three teams, one state table.

FAQ

Stop the action and return a typed, non-retryable error. Don’t try another tool that reaches the same outcome, and surface the session to a human. The MCP specification suggests offering alternatives on decline; for agents holding shells and browsers, that is how a refusal gets routed around, so this rule is deliberately stricter.

No. The 2026-07-28 specification says hosts must obtain explicit user consent before invoking any tool, and also that MCP itself cannot enforce these principles at the protocol level. Enforcement belongs to the host application: check consent before every tool call, and treat decline, cancel and unknown as stops.

No. The Digital Omnibus on AI, in force since July 27, 2026, moved EU AI Act high-risk deadlines to December 2027 and August 2028; it did not change GDPR consent. The separate cookie and GDPR proposal, including single-click refusal, is still a proposal, and the Council dropped its browser-signal article.

Sources

YOU'RE THROUGH THIS ONE.

Keep connecting the dots.

Back to the library