{"slug": "the-approval-signature-that-could-be-spent-twice", "title": "The approval signature that could be spent twice", "summary": "A developer's solo hackathon project, Syntrueno, revealed two critical concurrency bugs in its human approval gate and audit ledger, allowing a one-time approval signature to be spent twice and causing the ledger to fork. The developer fixed the issues by reordering operations and adding locks, and emphasizes the importance of testing for race conditions over trusting architectural diagrams.", "body_md": "I built this project and wrote this article for the purposes of entering the Google Cloud \"All Things Agentic\" Hackathon 2026.\n\nMy human approval gate checked its one time signature, ran the action, then marked the signature used. Two operators double clicking Execute at the same moment is enough to run the action twice.\n\nIt is fixed now, and it is still the most useful thing I learned building **Syntrueno**, my solo entry for Track 3 of the hackathon. An incident arrives, sometimes from a Cloud Monitoring alert with nobody in the loop. Agents diagnose it, a judge scores the proposed fix against a safety rubric, and anything consequential stops until a human signs for it.\n\nThat part I can draw on a whiteboard. What actually taught me something was two bugs, neither of them in the interesting layer.\n\nThe gate mints a SHA-256 signature bound to one exact action and one exact parameter set, good for a single use, expiring after 30 minutes. The order of operations was: check it, run the mutation, then spend it.\n\nFastAPI serves sync endpoints from a threadpool, so two requests arriving together both got a yes, both wrote to Cloud Run, and the ledger recorded one event. One signature, two mutations, one audit entry.\n\nThe fix was ordering, not cleverness. Find and spend the signature under one lock, and spend it *before* the mutation rather than after. If the call raises before anything changed, hand the signature back so a transient error does not cost the operator their approval. There is now a test that fires two requests at one signature and expects a single execution.\n\nEvery outcome, including every refusal, is appended to a hash chained ledger, and that append was a read, modify, write over the chain head with no lock. Same threadpool, same shape: two incidents interleave, both claim the same position, the chain forks, and nothing raises. A verifier would report a broken chain long after the run that broke it.\n\nUnderneath it, the health check had been answering the wrong question. Connected meant a client object existed, not that writes were landing, so the stores had quietly fallen back to memory while still reporting themselves durable. A system that claims durable storage while writing to a dictionary in RAM is the exact failure this project exists to be incapable of.\n\nNeither bug announced itself, which is why I trust the test suite more than the diagram. Pytest collects 284 tests. They pass offline in about three seconds with no API key and no cloud credentials, and CI runs them on a machine deliberately given no secret. If a test can only pass when the cloud is up, it was never testing my code.\n\nThe agent's action space is a closed enum, handed to Gemini as its structured response schema. There is no destructive verb in it. Not disabled, not on a denylist. Absent. A prompt injection that fully succeeds still cannot emit one, because no delete verb is blocked; none exists.\n\nA blocked path can be reached by a bug. An absent one cannot.\n\nThe honest limit on that: the enum still contains verbs that change a live service, and a wrong parameter on one of those can still hurt. It bounds the blast radius, it does not remove it. The property rests on every member of that enum being safe under every parameter set, which is a claim about what I put in my enum rather than a result from type theory. The two bugs above are part of what keeping that claim true actually costs.\n\nThe same outage alert, sent twice.\n\nOnce with a prompt injection buried in the error text, which three screens, one of them Model Armor, quarantine.\n\nOnce quoting a real `DROP TABLE`\n\nfrom a slow query log, which is a database engineer's evidence about what broke, and which passes straight through to the agent.\n\nInstructions are stopped. Evidence gets through. That distinction is the whole design.\n\nIt is a hackathon project, built solo, with no users. What stays with me is the habit of asking what a green check is actually measuring. Both of these bugs sat behind something that looked fine.\n\nBuilt on Cloud Run, Firestore and Vertex AI, with Gemini and Gemma 4.\n\nLive service: [https://syntrueno-18489510475.us-central1.run.app](https://syntrueno-18489510475.us-central1.run.app)\n\nCode, Apache 2.0: [https://github.com/Shaan-alpha/syntrueno](https://github.com/Shaan-alpha/syntrueno)", "url": "https://wpnews.pro/news/the-approval-signature-that-could-be-spent-twice", "canonical_source": "https://dev.to/shaanalpha/the-approval-signature-that-could-be-spent-twice-cdj", "published_at": "2026-08-30 02:21:11+00:00", "updated_at": "2026-08-30 02:51:57.770822+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "developer-tools", "ai-infrastructure"], "entities": ["Syntrueno", "Google Cloud", "Gemini", "Model Armor", "FastAPI", "Cloud Run", "Cloud Monitoring", "Pytest"], "alternates": {"html": "https://wpnews.pro/news/the-approval-signature-that-could-be-spent-twice", "markdown": "https://wpnews.pro/news/the-approval-signature-that-could-be-spent-twice.md", "text": "https://wpnews.pro/news/the-approval-signature-that-could-be-spent-twice.txt", "jsonld": "https://wpnews.pro/news/the-approval-signature-that-could-be-spent-twice.jsonld"}}