AFP engineering series · Chapter 9 of 25

Build confirmation flows that resist changed arguments, stale permissions, replay, and misleading approval screens.

A yes button is not a complete authorization protocol

An assistant proposes a refund of 500 minor units to the original payment method. A user approves. Before execution, a later model step changes the amount or destination. If the application stores only approved=true on the conversation, the changed action may inherit the earlier approval. The user approved one proposal; the system executed another.

ADK's confirmation feature can pause tool execution, including a Python FunctionTool configured with require_confirmation=True. That provides an interaction mechanism. The application still needs to authenticate the approver, bind the decision to specific arguments, enforce freshness, and prevent duplicate execution.

Show the fields that determine consequence

An approval screen should display action type, resource, amount, currency, destination, and any irreversible effect. Use values from the canonical server-side proposal, not a model-written summary of them. A summary can help explain the action, but it must not be the only thing the approver sees.

For a destination change, show both the previous and proposed destination in an appropriately masked form. For bulk messaging, show recipient count and the actual message preview. For document deletion, show the affected scope and recovery behavior. The same generic “continue?” prompt is inadequate for actions with different consequences.

Create a canonical action record

Illustrative python; adapt the persistence and identity adapters to your service.

import hashlib, json

def action_digest(action):
    canonical = json.dumps(action, sort_keys=True,
                           separators=(",", ":"), ensure_ascii=False)
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

proposal = {"operation": "refund", "order": "fixture-42",
            "amount_minor": 500, "currency": "INR",
            "destination_id": "original-payment"}
approved_digest = action_digest(proposal)
changed = dict(proposal, amount_minor=900)
assert action_digest(changed) != approved_digest

The digest detects a changed canonical proposal; it does not authenticate the approver. Store approval in a protected service record with approver identity, proposal version, expiry, policy version, and operation identifier. Validate the input schema before canonicalization so ambiguous numeric or Unicode representations do not create multiple interpretations of the same action.

At execution, load the proposal by identifier and compare its current version with the approved version. Avoid accepting a full replacement proposal from the client together with a digest and assuming that the client honestly computed both. The server owns the action record and its state transitions.

Revalidate after the pause

Time passes during review. The order can be refunded elsewhere, the approver can lose permission, the destination can be disabled, or the policy can change. Recheck current eligibility and authorization immediately before reserving or committing the action. An approval is evidence of intent at a moment in time, not an exemption from current constraints.

Define expiry according to the workflow rather than copying a universal duration. A low-risk draft review may remain useful for hours; a payment approval tied to a volatile balance may require a shorter window. If a proposal changes, show the new version and request a new decision instead of silently extending the old approval.

Make resumption idempotent

The browser may submit approval twice, a worker may retry, or the runtime may resume after a crash. Use a stable operation identifier and a state machine that permits at most one execution transition. An idempotency key at the external processor helps deduplicate transport retries, but local reservation and reconciliation still matter.

If the processor times out, mark the operation uncertain and query its status using the same operation identity. Do not treat “no response” as “nothing happened.” The assistant should communicate the authoritative pending state rather than generate a confident success or failure statement based on a timeout exception.

Test the approval contract

CaseExpected result
Same proposal, valid approver, fresh approvalEligible for final service checks
Amount or recipient changedApproval invalidated
Approval submitted twiceOne logical operation
Approver permission revokedExecution denied
Processor timeout after acceptanceUncertain state, reconcile without new payment

Include browser-level tests that verify the displayed fields match the action record. A backend digest test will not catch an approval screen that accidentally hides the destination or shows an outdated amount. Test keyboard access and clear cancellation so users can decline without ambiguity.

For high-impact operations, consider an independent reviewer or stronger authentication based on the product's risk model. The assistant must not choose the reviewer from untrusted document text. Reviewer assignment and escalation should come from organizational policy and authenticated records.

Worked approval race: changed amount after review

A reviewer sees a proposal to refund 50 units to the original payment method. The proposal has version 3. The reviewer approves that version, and the approval service stores its digest and expiry. Before execution, a background agent updates the proposal to 80 units because it discovered another line item. The application must not treat the earlier approval as approval for version 4.

At execution, the service loads the current proposal and compares its version and digest with the approval record. A mismatch returns reapproval_required and leaves the ledger unchanged. The assistant can explain that the proposal changed and present the new values. It cannot repair the mismatch by rewriting the approval record or telling the service that the reviewer probably intended the larger amount.

Now submit the original approval twice from two browser tabs. The approval endpoint should identify the same decision or reject an already-finalized transition. The execution endpoint should use a stable operation identity and atomic state transition so duplicate resume requests do not cause duplicate payments. These are separate protections: approval deduplication alone does not serialize payment execution.

Next, let the approval expire while the case waits in a queue. The worker must evaluate freshness when it executes, not only when it enqueues. A queue timestamp is not evidence of current permission. If the approver loses the relevant role, the policy should also define whether existing approvals remain valid; high-impact workflows commonly need a current authorization check.

Test the interface with a changed currency and destination as well as a changed amount. A digest mismatch is useful only if the canonical action includes every consequential field. Excluding an attachment, recipient set, or account identifier can leave a gap even when the amount is protected.

The acceptance evidence consists of screenshots or UI assertions showing the exact proposal, service tests proving mismatches deny execution, and concurrent tests proving one logical effect. ADK's confirmation pause helps coordinate the interaction, but the evidence demonstrates the application protocol that gives the user's approval its precise meaning.

Integrate with ADK without overclaiming

Wrap the relevant tool with confirmation and keep the final execution endpoint responsible for checking the protected approval record. Test pause and resume behavior in the actual deployment mode, including persistence and session ownership. A demo using in-memory state does not establish that approvals survive restarts safely.

The refund lab makes amount binding, expiry, tenant checks, and replay visible as separate controls. It uses synthetic ledger entries and does not move money. Its purpose is to show why human confirmation is one stage in a transaction protocol. The production guarantee comes from the service state machine and the evidence that every execution path obeys it.

Sources and implementation scope

Primary references checked October 15, 2026. Framework details follow the linked documentation; the Northstar cases, policy contracts, and test designs are original illustrative designs. Python snippets labeled illustrative are not complete production integrations. ADK package metadata checked for this series reported 2.12.0; pin and test the version used by your application.

Protect the action boundary with verified evidence, and test both containment and legitimate use.