What you'll learn: how the capstone's ApprovalGate keeps a model from creating a quote draft on its own, why an approval is bound to a digest of the exact arguments, and how to walk one request through pending, approved and used.
At a glance
Figure 1. A sensitive action behind an approval gate
How to read this: the model's proposal can only become a pending request; the draft is created inside the gate, after a different person approves.
- An action that changes the world needs a person's approval, checked in code and not requested in the prompt.
- The approval is bound to a SHA-256 digest of the exact arguments, so changing one value voids it.
- An approval is used once, and the requester cannot approve their own request.
Scenario
Composite scenario, not a real company. At Lumen Industrial Supply, a fictional distributor, the assistant can read the catalog and may also propose a quote draft: 40 vibration sensors for one customer reference.
The rule: the model may propose, a different sales person must approve, and the action that runs must be the one approved. Numbers are illustrative.
What you will be able to do
- Explain why approval is enforced in code and bound to the exact arguments.
- Walk a request through
pending,approvedandused, and name the call for each change. - Show with a test that changed arguments, a second use and a self-approval are refused.
- Name what the gate lacks: expiry, persistence and a block on
approveafter use.
Prerequisites
- Permissions the application enforces for the
create_quote_draftandapprove_quote_draftroles, and Limits, state and step recording for the runner. - Theory: Agents, tools and MCP.
Core ideas
Propose, approve, execute
Figure 2. One quote draft from proposal to creation
How to read this: the model appears only on the first line; everything after it is application code and a person.
The runner never executes the action. On request_approval it validates the arguments, asks the gate for an approval and stops with the status awaiting_approval. The model cannot call create_quote_draft as a tool either: the registry refuses tools that are not read-only.
Nothing in the capstone's API can create a draft. A real service would add its own authenticated route that calls execute.
Bind the approval to the exact call
Approving "a quote draft" is not enough, because the arguments could change before execution (a retry, a model edit, a bug). So the gate approves a fingerprint of the call.
Verified: capstone/fde-product-discovery-copilot/app/workflow/approval.py
class QuoteDraftArgs(BaseModel):
sku: str = Field(pattern=r"^LUM-[A-Z]{2}-\d{3}quot;)
quantity: int = Field(gt=0, le=10_000)
customer_ref: str = Field(min_length=1, max_length=60)
def args_digest(action: str, args: QuoteDraftArgs) -> str:
canonical = json.dumps({"action": action, "args": args.model_dump()}, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode()).hexdigest()
The digest covers the action name and every argument. sort_keys=True makes the JSON independent of key order, and the compact separators remove whitespace. SHA-256 gives the same 64 hex characters for the same input. It is a fingerprint, not a secret.
Predict first. Alice requests a draft for 40 units. A bug changes the quantity to 4,000 before execution. The approval was granted for 40. What happens at execute, and what is the approval's status afterwards?
Answer
execute raises ApprovalError with "These arguments are not the ones that were approved." No draft is created. The status stays approved, because a refusal does not use the approval up, so the correct call with 40 units still works (test_changed_arguments_are_refused_and_the_approval_survives).
Three states, one rule each
Figure 3. States of one approval