Skip to main content

once-gate

Make your AI agent's side effects happen at most once, and only the exact call a person approved.

Agents retry. Frameworks replay. Networks drop the answer after the action already happened. The result is the email sent twice, the card charged twice, the order placed twice. once-gate sits between your agent and its tools and makes each real-world action run at most once.

agent  ──►  once-gate  ──►  your MCP server (send_email, charge_card, create_order …)
             │
             └─ claim table (SQLite) + append-only log

What it does

For each gated tool call:

Situation What once-gate does
First time this action is seen Claims it, forwards it, stores the result
Same action again (same tool + same arguments, or same idempotency_key) Returns the stored result. The server is not called.
Same action arrives while the first is still running Waits and gives it the first call's answer
Action started but never finished (crash, timeout, server error) Refuses with INDETERMINATE. The action may or may not have happened, so a human checks and then runs oncegate release KEY

The last row is the point. Most retry logic assumes a failed call didn't happen. For side effects, that's how you charge someone twice. once-gate never re-runs an unfinished action on its own.

New in 0.2.0

  • Exact-content human approval for the tools you choose (--require-approval).
  • Relay rejection: nothing inside the MCP message stream can approve anything.
  • Last-second STOP file (--stop-file), checked immediately before a gated call is forwarded, fail-closed.
  • Receipts: every ledger record carries decision, reason, action_hash, tool and at, so refusals are recorded too.

Use it as an MCP proxy

python -m oncegate proxy --db claims.db --ledger actions.jsonl --tools send_email,charge_card -- <your MCP server command>

Point your agent's MCP config at that command instead of the server's. Only the tools you list are gated (omit --tools to gate everything). Every message passes through unchanged except gated tools/call requests.

Pass an idempotency_key argument to define "the same action" yourself (for example an order number). Otherwise the key is the tool name plus the canonical JSON of the arguments.

Exact-content human approval

python -m oncegate proxy --db claims.db --ledger actions.jsonl \
    --require-approval send_email,charge_card --stop-file ./STOP -- <your MCP server command>

For a tool in --require-approval, a call is forwarded only if a person approved that exact call: the approval is bound to the action hash, sha256 over the tool name and the canonical JSON of the full arguments (sorted keys, no whitespace; the same canonicalization as the claim key). Change one character of any argument and the hash changes, so it needs a new approval. Approvals are:

  • single use: spent when the call runs;
  • time-limited: 24 hours by default (--ttl-hours);
  • created only by the CLI, never by the proxy.

Without a valid approval the proxy does not forward the call. It answers with a JSON-RPC error (code -32011) whose data holds code: "APPROVAL_REQUIRED", the action_hash, the tool, the exact arguments and a one-line summary, and it records the call as pending. Tools in --require-approval are also gated at-most-once, even if they are not in --tools. A repeat of an action that already completed is answered from the stored result and needs no new approval, because nothing runs.

A person approves:

python -m oncegate approve --db claims.db --pending            # list blocked calls, pick one, review, confirm
python -m oncegate approve --db claims.db --tool send_email --args-json '{"to": "a@example.com", "body": "hi"}'
python -m oncegate list --db claims.db --approvals             # approvals and whether they were used

approve prints the full exact payload and the action hash, then asks the person to type the first 8 characters of the hash. With no interactive terminal (stdin and stdout must both be a terminal; on Windows, a real console) it refuses, unless --i-am-a-human is given (with --pending, pick the call with --hash).

This is a speed bump, not a cryptographic guarantee. It stops an agent from approving its own calls through the tool channel, and makes a careless approval harder. It does not stop a process that can run oncegate approve --i-am-a-human, or write to the SQLite file, from creating an approval. If that matters to you, run the agent so it can do neither (separate OS user, no shell on the gate's machine, database outside its reach).

Relay rejection

Approvals never travel through the MCP message stream. Any field in a tools/call request whose name contains approv (for example approval, approved, approval_token, human_approved), at any depth in the arguments or elsewhere in params such as _meta, grants nothing. It is logged as relayed_approval_ignored with the field paths. Because such a field is part of the arguments, it is also part of the action hash, so a person sees it when approving.

STOP file

With --stop-file PATH, the proxy checks the path immediately before forwarding each gated call (after the claim and approval checks, as the last step). If the file exists it answers STOPPED (code -32012) and does not forward. The check is fail-closed: if it cannot be completed (permission denied, invalid path, I/O error) the call is treated as stopped. Because a stopped call was provably never forwarded, its claim and its approval are rolled back; once the file is removed the same call can run. Ungated tools are not affected.

Use it as a library

from oncegate import ApprovalRequired, Gate, Indeterminate, Stopped

gate = Gate("claims.db", "actions.jsonl", require_approval={"charge_card"}, stop_file="STOP")
receipt = gate.run("charge_card", lambda: stripe_charge(order), {"order": order.id}, key=f"order-{order.id}")

run raises ApprovalRequired, Stopped or Indeterminate without calling the function. Approvals for library use are created with the same oncegate approve command against the same database.

Receipts (the ledger)

--ledger is an append-only JSONL file. Every record has:

Field Meaning
event claimed, done, failed, replayed, refused_indeterminate, approval_required, stopped, relayed_approval_ignored
decision allowed, replayed or refused; null on outcome records (done, failed) and informational ones
reason for example first_run, approved, already_done, joined_inflight, indeterminate, approval_required, stop_file_present, stop_check_error: ...
action_hash the exact-content hash described above
tool tool name
at RFC 3339 UTC timestamp

Relation to the ACTA draft. The IETF Internet-Draft Signed Decision Receipts for Machine-to-Machine Access Control (draft-farley-acta-signed-receipts-03; an individual submission, not an IETF standard) defines receipt fields including decision, reason and tool_name (section 3.1.1), and issued_at and action_ref (section 2.2). once-gate uses the same names for decision and reason, and writes tool_name and issued_at alongside tool and at with the same values. Not aligned: once-gate's decision values (allowed/replayed/refused) differ from the draft's (allow/deny/rate_limit/require_approval); action_hash is not called action_ref because the draft defines a different hash preimage; and once-gate records are not signed and lack the draft's envelope and issuer fields. The ledger is a plain local log, not an ACTA receipt.

Inspect and recover

python -m oncegate list --db claims.db                 # every claim and its state
python -m oncegate list --db claims.db --pending       # calls blocked waiting for approval
python -m oncegate release --db claims.db "charge_card:order-17"   # after checking by hand that it did NOT happen

DONE actions can never be released.

Guarantees and limits

  • Exactly one winner per key. The claim is a primary-key insert in SQLite (tested across 8 concurrent processes).
  • At most once, not exactly once. If the server did the work but the answer was lost, once-gate says INDETERMINATE instead of guessing. That is deliberate.
  • Tool-level only. It can't see side effects a tool performs outside the call.
  • Single machine. One SQLite file per deployment; it is not a distributed lock.
  • Approval is a speed bump, not cryptography. Approvals are not signed and are only as safe as access to the database file and the oncegate command.
  • Approval matches bytes, not meaning. Two calls that mean the same thing but differ in any argument need separate approvals, and an approval says nothing about whether the call is wise.
  • The ledger is not tamper-evident. It is append-only by convention, not hash-chained or signed.
  • STOP is checked per gated call, before forwarding. It does not interrupt a call already running and does not apply to ungated tools.

Version 0.2.0. Built by Feenmile.

License

MIT

Metadata

Release files for once-gate 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for once-gate 0.2.0
File Size Uploaded Provenance Actions
once_gate-0.2.0.tar.gz 21.8 kB With provenance Details for once_gate-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for once-gate 0.2.0
File Interpreter ABI Platform Actions
once_gate-0.2.0-py3-none-any.whl Python 3 none any Details for once_gate-0.2.0-py3-none-any.whl

Total release size: 40.7 kB

Release files / once_gate-0.2.0.tar.gz

Download URL once_gate-0.2.0.tar.gz
Size 21.8 kB
Tags Source
SHA-256 checksum
How to use checksums
5036a891a8b3508c05afad8b830a1ac658cddaff2741b26611de4d90da5ae2e6
BLAKE2b-256 checksum
How to use checksums
5de2421d99a289183249cc55415186a8aa6be97cb8a68902113bab87708d3eb6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / once_gate-0.2.0-py3-none-any.whl

Download URL once_gate-0.2.0-py3-none-any.whl
Size 19.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
90bd198fbdd4a58fb7b98d1303417bd0ec7f20f31eda745d7b6d105d651cddbd
BLAKE2b-256 checksum
How to use checksums
7d313488eed21ddb95209660db585d80dd6ebbbf28dc37a6eff04277258e9b95
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Security for once-gate 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for once_gate-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page