Notes
Stale approvals: valid when requested, but is an approval clicked hours later still safe?
For enterprise architects, engineering leads, and IT audit teams: how to prevent Time-of-Check to Time-of-Use (TOCTOU) disasters caused by human approval latency when agents interface with business systems.
GembaOS is a zero-trust governance runtime for enterprise AI agents: policy-decided access, single-use clearance tokens bound to exact parameters, and accountable human approvals, running on-premises or in a private VPC.
This article examines one of the most critical yet overlooked vulnerabilities in enterprise AI automation: the state drift window (Time-of-Check to Time-of-Use, TOCTOU) between when an agent requests permission and when a human reviewer actually approves it hours later.
Incident breakdown: The “blind resume” trap in current Agent products
At 10:00 AM on Wednesday, a customer service agent handling an escalated dispute posts a tool-call approval request to a Slack channel or developer CLI: “Requesting permission to refund $450.00 for Order #8821. Approve? [Allow / Deny]”.
At 10:00 AM, the agent’s premise is completely valid: the CRM confirms the order is unrefunded, the complaint is verified, and the amount complies with refund policy.
However, in real enterprise operations, human managers do not sit waiting at terminals 24/7:
- The customer service manager is in a two-hour cross-functional executive review;
- During the two hours the request sits unreviewed, the customer walks into a retail store at 10:40 AM, and an in-store cashier completes a full cash refund on the physical POS terminal;
- Or at 11:00 AM, an operations engineer investigating dispute logs manually marks the order as resolved and adjusts the ledger.
At 12:00 PM, the manager finishes their meeting, returns to their desk, and sees the pending Slack notification from two hours ago: “Agent requests $450.00 refund”. Recalling the case context, the manager clicks [Allow].
In almost all current agent frameworks and products, this is where disaster strikes:
Existing agent architectures treat clicking Allow merely as an asynchronous “unpause” signal for the worker thread. Once the button is pressed, the agent blindly resumes and fires the exact API payload it constructed two hours earlier—issuing a second $450.00 refund against an order that was already settled in cash.
This failure did not stem from LLM hallucination or prompt injection. The manager’s decision was logical based on the two-hour-old summary, and the agent stayed within its assigned tools. Rather, an unavoidable human latency window elapsed between proposal and sign-off, and the native agent blindly trusted an obsolete snapshot of reality.
| How state goes stale | Practical enterprise scenario | Root cause |
|---|---|---|
| Human absence / Latency | Agent emits approval prompt; manager reviews and clicks Allow hours later after offline staff already resolved it | Asynchronous human delay |
| Concurrent data modification | A human teammate edits the amount, shipping address, or account before Allow is clicked | Manual conflict |
| Multi-agent race condition | A second automated workflow acts on the same record during the approval window | Race condition |
| Network retries | Gateway timeouts trigger automatic retry loops, replaying an already completed write | Infrastructure replay |
What a payment API already protects, and what it cannot
The usual objection to the story above: “a payment gateway has idempotency keys and a refund limit; it will not refund twice.” True, and not enough. The gateway stops the two things it can see; the incident turns on a third it cannot.
| Protection | Who provides it | Stops | Does not stop |
|---|---|---|---|
| Retry deduplication (idempotency key) | Payment gateways and other APIs that offer it | The same request resent after a timeout | A second request assembled two hours ago and sent under a new key |
| Amount limit (refundable balance) | Payment gateways | A refund larger than the charge | Two $450 refunds on a $1,000 order; a card refund on an order already refunded in cash at the store |
| Whether the approval’s facts still hold | Only the caller holding the approval | Order closed in the CRM, amount edited by a colleague, cash refund at the store | — |
The gateway knows “this card payment still has $450 refundable”; it does not know the customer took cash at 10:40. The third layer can only be checked by whoever holds the approval, by going back to the CRM and the order record before writing. GembaOS does all three: where the target honors an idempotency key it uses the target’s (the key is generated by GembaOS from the approval and work item ids); where it does not, GembaOS itself guarantees one send. The limit is read from the target before the write and compared, and where the target enforces it too the record says so. The approval’s facts are re-read field by field before the write.
Human approval does not equal blind execution authority
Many enterprise workflows mistake clicking [Allow] in chat tools or an “Approved” ticket status for an immediate green light to execute. Conflating these two concepts creates severe production vulnerabilities.
An approval establishes that a business decision is sound at a point in time. The governance runtime must separately guarantee that this write may occur right now, against this exact live state, and strictly once.
| Core Question | Answered by Human Approval | Answered by GembaOS Runtime Gate |
|---|---|---|
| Is this decision strategically and legally sound? | Yes (signed by an accountable person) | — |
| Are the facts the approver saw still true right now? | No (reality moved during the meeting) | Runtime preflight live state re-read |
| Is the record currently locked and safe to write? | No | Row lease lock & adapter concurrency guard |
| Does the amount remain within real-time authority limits? | No | Pre-write live limit re-read from source |
| Is the action guaranteed to execute at most once? | No | Single-use clearance token & kernel idempotency key |
How GembaOS narrows the stale approval window
GembaOS starts from the engineering reality that human sign-off is inherently asynchronous (minutes, hours, or overnight). Therefore, GembaOS never treats an approval click as an unconditional trigger to execute stale payloads:
[Agent Emits Tool Call]
│ (with underlying data version & precondition assertions)
▼
┌──────────────┐ ┌──────────────────────────┐
│ AI Assembles │ ───> │ Human Asynchronous Delay │
│ Baseline │ │ (In meeting / Off-desk) │
└──────────────┘ └──────────────────────────┘
│
[Human Signs Off]
│
▼
┌──────────────────────────┐
│ Model Completely Exits │
│ the Execution Call Path │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ GembaOS Governance Gate │
│ 1. Re-read live database │
│ 2. Check assertions diff │
│ 3. Validate Token TTL │
│ 4. Acquire lease lock │
│ 5. Inject IdempotencyKey │
└──────────────────────────┘
│
[Live State Matches Proof]
▼
┌──────────────────────────┐
│ Call Target API Safely │
└──────────────────────────┘
| Potential Risk Point | Governance Kernel Defense Mechanism |
|---|---|
| Preconditions drift during review | The governance kernel directly re-queries live database state (status, balance, modified fields) against signed assertions; if state changed (e.g. counter refund occurred), execution halts, the token is voided, and the diff is flagged. |
| Clearance token expiration | The clearance token generated upon approval carries a rigid time-to-live (e.g. 30 minutes). Expired tokens cannot execute, blocking historical approvals from lingering. |
| Concurrent edits & overwrites | Where the target supports a version or a conditional write, the write carries the version or expected values read at approval and the target refuses a stale one; where it supports nothing, GembaOS serialises the writes it mediates and re-reads inside the lease, with the window in milliseconds on the chain. |
| Budget or quota changes | Live limits are re-read from the source system immediately before invocation, preventing overruns against stale snapshot limits. |
| Duplicate network retries | The clearance token is single-use; the idempotency key is generated by the kernel and cannot be chosen by the agent. Where the target honors the key, a retry lands on the same execution; where it does not, a timeout is not retried and the execution is marked unverified for a person. |
As much protection as the target gives, and what GembaOS does with the rest
Every connected write declares two things in its interface contract: whether a retry is safe (idempotent, idempotent by key, must not be retried), and whether the target can refuse a write based on a stale read (a version, a conditional write, nothing). GembaOS picks its behavior from the declaration and writes which one it picked into the record of that execution.
| What the target can do | Typical interfaces | What GembaOS does |
|---|---|---|
| A version (ETag, If-Match, last-modified) | Salesforce, kintone, an in-house API with a revision field | The write carries the version read at approval; the target refuses a stale one and the approval request goes back |
| A conditional write (“update only if status is still X”) | A conditional UPDATE or stored procedure on your own database | The write carries the expected values from approval time; the target checks them |
| Nothing | An in-house ERP or CRM REST endpoint, a legacy system updated directly by SQL, a payment file (an SFTP batch), a SaaS without conditional updates such as HubSpot, an MES/WMS hold release, a directory password reset, sending email | GembaOS serialises the writes it mediates on that record, re-reads, compares and writes inside the lease, then reads the record back; the milliseconds between re-read and write go on the chain |
The third row is most of an enterprise. Nine of the twelve connection configurations that ship with GembaOS declare “nothing”, because that is what the systems behind them offer. A payment file has no notion of an idempotency key at all: drop the same file twice and it pays twice, so it is declared “must not be retried” and GembaOS does not drop it again after a timeout.
What is guaranteed, and where the window is narrowed rather than closed
Against a target with no protection at all, three things still hold:
- One send. A clearance is consumed once and execution runs reserve → execute → consume; an operation declared non-retryable is not resent after a timeout, the execution is marked unverified and goes on the gap register for the workflow’s accountable person to check.
- No write on changed facts. Before writing, GembaOS re-reads the record itself and compares it field by field with what the approver signed; a difference voids the clearance and returns the approval request.
- GembaOS-mediated writes do not trample each other. Two approved actions on the same record: one holds the lease while the other waits, and one that is still waiting when its clearance expires is returned.
Narrowed rather than closed is one thing only: a writer outside GembaOS — the cashier at the store, a colleague editing the record in the ERP screen — landing in the tens of milliseconds between the re-read and the write. GembaOS does not claim that window is closed; it records it. Every execution’s ExecutionChecked event states which protection held the write (the target’s version, its conditional check, or GembaOS’s own lease), how many milliseconds the window was, and what the read-back after the write showed. A read-back that differs or fails does not pass quietly; it becomes an unverified record that a person has to look at.
What the approver sees before signing
This governance boundary is not an invisible backend detail; it is explicitly rendered on the approver’s interface:

The approval card highlights these safeguards before signing:
- Time window: Exactly how long the business endorsement remains valid (e.g., 30 minutes);
- Execution limit: Capped strictly at 1 write to eliminate duplicate charges;
- Preflight verification items: Explicit checklist of fields that the kernel will re-verify against the live database before touching the payment API.
Summary
Approval latency is not a minor UX inconvenience—it is a core systems correctness problem.
Whenever human beings participate in approvals, time elapses. During that elapsed time, records mutate, colleagues make concurrent edits, budget limits change, and networks trigger retries.
Traditional agent setups that stop at “the human clicked Allow in Slack” fail to solve any of these issues. GembaOS’s answer is clear: treat approval as a point-in-time business endorsement, and enforce a runtime governance kernel that re-verifies live reality before a single byte is written, and leaves a record for a person wherever it could not verify.
Questions this note answers
What happens if business data changes while waiting for approval or after signing?
Execution halts immediately. Before dispatching calls to underlying business APIs, the GembaOS governance kernel re-reads the live database and compares current values against the signed preconditions. If a compared field has drifted (e.g., refunded offline or edited by a colleague), execution is aborted, the clearance token is voided, and the work item is returned to the approver with the diff highlighted.
Can an agent execute an action after the request sat waiting for hours?
Not if reality drifted or the token expired. The single-use clearance token carries a strict time-to-live (e.g., 30 minutes for refunds). More importantly, the runtime re-checks live database state prior to write; if preconditions drifted while the manager was away, execution is immediately blocked and returned.
Can the same approved action accidentally run twice?
GembaOS never sends it twice: the clearance is consumed once and the idempotency key is generated by the kernel from the approval and work item ids, never chosen by the agent. Where the target honors that key (a payment gateway), a retry lands on the same execution; where it does not (a payment file, an in-house endpoint), a timeout is not retried blindly — the execution is marked unverified and goes to the accountable person.
What happens if two operations touch the same record simultaneously?
It depends on what the target can do. Where it supports a version or a conditional write (Salesforce, a conditional update on your own database), the write carries the version read at approval and the target refuses a stale one. Where it supports nothing, GembaOS serialises the writes it mediates on that record, re-reads inside the lease and writes, and records the milliseconds between re-read and write on the chain; against writers outside GembaOS that window is narrowed, not closed.
Sources
As of September 2026 GembaOS holds no certification; the control mappings on this site are self-assessed, not audited. GembaOS is in its design partner stage. Read about the design partner program.
Last updated: · GembaOS v0.2