androidengineers.Book a session

Human oversight and action security

Lab: test an approval state machine

exerciseSelf-paced

Assignment

Create a local proposal API with two users, two tickets, immutable proposal records, and a fake write adapter.

Build sequence

  1. Implement approve, reject, expire, and apply transitions.
  2. Test changes to content, target, identity, and resource version after approval.
  3. Attempt duplicate application and application after rejection.
  4. Capture an audit record linking the approver, proposal, and final effect.

Acceptance checks

  • No mutation occurs for changed, rejected, expired, or unauthorized proposals.
  • Repeated application has at most one side effect.
  • The reviewer can understand the exact action without inspecting a model transcript.

Evidence to keep

Submit the working artifact or decision document described above, the inputs used, and the observed results for every acceptance check. Include one failed attempt and the change you made after investigating it. Label fixture-based outcomes separately from live-system measurements. These artifacts become part of your final portfolio review.

Guided workshop: bind approval to a versioned proposal

An approval must identify the exact proposed change, target resource, version, requester, approver, and expiry. Changing the target or text after review creates a different proposal. A model-produced sentence saying “the user approved” is not an approval event.

# academy-check
from copy import deepcopy

proposal = {"id": "p1", "ticket": "t1", "version": 4, "text": "Suggested reply",
            "state": "pending", "expires": 100}

def approve(value, actor, now):
    if actor != "reviewer" or now >= value["expires"] or value["state"] != "pending":
        raise ValueError("approval rejected")
    approved = deepcopy(value)
    approved["state"] = "approved"
    approved["approver"] = actor
    return approved

def may_apply(value, current_ticket_version, now):
    return (value["state"] == "approved" and value["version"] == current_ticket_version
            and now < value["expires"])

approved = approve(proposal, "reviewer", 10)
assert may_apply(approved, 4, 20)
assert not may_apply(approved, 5, 20)
assert not may_apply(approved, 4, 100)

The actor is a fixture identity. A deployed endpoint must derive it from authenticated session state and check resource-specific approval authority. Store proposals immutably or bind their approved representation to a digest verified by the server. A mutable dictionary supplied by a client is not sufficient.

Exercise: implement pending, approved, rejected, expired, applied, and failed transitions in SQLite. Use a conditional update on the expected state and version. Apply the mutation and operation ledger in the same transaction when they share a database. Replay should return the original application result rather than perform another write.

Adversarial cases: changed text, changed ticket ID, revoked approver permission, expired proposal, duplicate approval, rejection followed by application, stale ticket version, and model-supplied approver identity. Assert zero writes for every denied case.

Worked outcome: when a ticket changes after review, keep the old proposal as historical evidence but require a new proposal and approval. Do not silently rebase a person's authorization onto different content.

Checkpoint: show an audit record connecting the request, proposal, human decision, and applied operation without storing credentials. Explain which checks must happen again immediately before mutation, even if they passed when the proposal was created.

YOUR LEARNING JOURNEY

0 of 121 available lessons completed

Progress saved in this browser. No account needed.
Lab: test an approval state machine | Agentic AI | Android Engineers