An approval is bound to the exact thing it approves, and written down first

When an agent can spend money, the approval has to name the exact request, and the record of the spend has to exist before the spend does.

The two systems this comes from are private projects of mine, so none of their counts, prices or rates appear here. The method is what a reader can check, and both checks below use only the standard library and print their own output.

Check this yourself

The first records a hash of the exact request when a person approves it. The executor recomputes the hash before it acts and refuses a request that has changed by even one word.

import hashlib

approved = "a heron at dawn, soft light, no text"
token = hashlib.sha256(approved.encode()).hexdigest()   # recorded with the approval

def run(request):
    if hashlib.sha256(request.encode()).hexdigest() != token:
        return "refused: changed after approval"
    return "ran"

print(token[:16])
print(run(approved))
print(run(approved.replace("dawn", "dusk")))
af65d711c39a87ac
ran
refused: changed after approval

This hashes the plain text, which shows the principle. My real systems fingerprint the request in their own ways, and I do not claim a single algorithm end to end.

The second kills a process right after it creates a pretend VM. In the first order the process acts and then writes the record; in the second it writes the record and then acts. At the next start, a request with no matching delete sends a sweep after everything carrying the queue’s label.

import json, os, subprocess, sys, tempfile

CHILD = """
import os, sys
ledger, vms, order = sys.argv[1:]
def write_ahead():
    open(ledger, "a").write('{"event": "requested"}\\n')
def create():
    open(vms + "/vm-1", "w").close()
    os._exit(1)   # killed right after the create call returned
if order == "ledger-first":
    write_ahead(); create()
else:
    create(); write_ahead()
"""

def after_kill(order):
    d = tempfile.mkdtemp()
    ledger, vms = d + "/ledger.jsonl", d + "/vms"
    os.mkdir(vms)
    subprocess.run([sys.executable, "-c", CHILD, ledger, vms, order])
    # the next start: a request with no matching delete means sweep everything we labelled
    events = [json.loads(l)["event"] for l in open(ledger)] if os.path.exists(ledger) else []
    if events.count("requested") > events.count("deleted"):
        for name in os.listdir(vms):
            os.remove(vms + "/" + name)
    print(f"{order}: VMs still running after the next start: {len(os.listdir(vms))}")

after_kill("act-first")
after_kill("ledger-first")
act-first: VMs still running after the next start: 1
ledger-first: VMs still running after the next start: 0

The VM is a file in a temporary folder. Nothing here touches a cloud.

The move

One idea in two places. For an image model, an agent can prepare and refine a request, but nothing runs until a person records an authorization that carries a SHA-256 fingerprint of the exact saved prompt. Revising the prompt makes a new request that needs a new authorization. The browser step that finally presses the button recomputes the prompt’s hash and refuses if it changed, and it stores a single-use marker, keyed to that authorization, before it clicks.

If the click gets no answer, the outcome is unknown: it may or may not have generated, and the message says to check before trying again. A timeout is not permission to generate again.

For a cloud GPU VM the same rule takes another shape. A ledger line saying a VM was requested is appended before the create call. If the process dies during the call, the next start sees a request with no delete and removes every VM carrying that queue’s label. The deadline is also not the app’s to keep: the VM is created with a maximum run time and an instruction to delete itself at the end, so a killed app cannot leave it running past that.

Why the obvious version fails

The obvious approval is a yes attached to a request id. Then the request can change after the yes, and the approval still applies to whatever it has become. Binding the approval to a hash of the content makes the edit visible at the one moment it matters.

The obvious record is a log line written after the action. The act-first run in the second block above is that version, and it leaves a running VM that nothing knows about. Writing first costs a stale line when the action never happens, and a stale line is the cheaper mistake.

The obvious answer to a lost reply is to retry. For a call that costs money, a retry is a second spend that looks like recovery.

What this does not cover

  • The allowance is the application’s own limit, not a billing cap at the provider, and the application says so.
  • A failed ledger write is logged, not blocking.
  • The allowance lives in memory, not on disk.
  • An app killed right after an approval can still cost VM time, because the VM’s run time has a fixed floor.
  • An unanswered click is unknown, not failed. It needs a person to look.
  • One operator.
  • The code is private, so nothing about the systems themselves can be re-run from here.

The same reasoning about what an agent is allowed to conclude is on how I verify agent work and agents that don’t trust each other. A tool that avoids the harm by having no path to it is on fail closed, and the timing method for the GPU side is on GPU profiling. Back to the home page.