CTRLRun Approval Console
Officialpip install ctrlrun && ctrlrun demoWhat it does
The model guesses. CTRLRun does not. The ticket says refund €500. The agent asks for €5,000, one extra zero. The tool is in its list, the arguments are well formed, and the model is completely confident. Nothing above the call disagrees, because nothing above the call is a check: a tool being callable is not permission to call it with those arguments.
Without CTRLRun | With CTRLRun |
Nothing checks the amount. The call goes through. €4,500 too much. | Your rule checks the amount. The call never leaves. €0 wrongly paid. |
CTRLRun is that check. It reads the arguments about to leave your process and answers what may happen to them: let it run, ask a human, or stop it cold. Four rules do the work, and each one is a test in this repository before it is a sentence here.
Exact means exact | Changed arguments need a new approval. |
Once stays once | Same effect key, shared store, no repeat. |
Unknown means wait | Confirm the outcome before retrying. |
Every answer is kept | Requests, decisions and results, refusals included. |
The third one is the half people forget. A correct €500 refund commits at the provider and the
reply is lost coming back, so the agent retries. Retry libraries, agent frameworks and tool
loops collapse this failed into I do not know what happened. CTRLRun keeps them apart: a
lost reply is AMBIGUOUS, never FAILED, and a retry against an AMBIGUOUS effect is refused
until a human, or a reconcile hook, says what happened.
$ ctrlrun demo
CTRLRun demo — five ways an agent action goes wrong, and what stops it.
Policy: refunds up to €1,000 are autonomous, up to €10,000 need a human, above that are denied.
1. Duplicate effect after a lost response
refund €500 → remote commits → response lost → effect: AMBIGUOUS
agent retries the same refund
✗ BLOCKED — effect may already have committed; blind retry refused
remote refund calls: 1
only a human moves it on: ctrlrun resolve refund:txn_1 --committed|--failed
2. Approval mutation
agent proposes refund €2,000 → human approves apr_0aa78e0380ba55d77a601dc782f57095 (bound to the action hash)
agent executes refund €5,000 →
✗ BLOCKED — approved action ≠ requested action (mismatch)
3. Concurrent agents, same effect
Agent A reserve refund:txn_123 → ACQUIRED → executes
Agent B reserve refund:txn_123 →
✗ BLOCKED — already reserved (in_progress)
4. Approval replay
approval apr_dbc8bc6f06690cdf2e2c55a4e591ef3b used once → consumed
same approval presented again →
✗ BLOCKED — single-use approval already consumed
5. Authority escalation
human €100,000 delegable → finance agent €25,000 → support agent €2,000
support agent's grant: dlg_5f8d41938a3f29972d5489d676cd9edb
support agent requests €50,000 →
✗ BLOCKED — outside the delegated grant (authority_constraint)
remote refund calls: 0
finance agent tries to delegate €50,000 under its own €25,000 → refused (containment: constraints)
support agent requests €1,500 → authority permits it, and the policy asks a human (apr_f86eca24dd80206ab5189ccb1b62aa55)
two axes, and an action needs both: the stricter of the pair wins
Receipts (8): .ctrlrun/demo/receipts.jsonl
Events: .ctrlrun/demo/events.jsonl
Read them: CTRLRUN_STATE=.ctrlrun/demo/state.db ctrlrun receiptsApproval and delegation ids are generated per run; everything else is exactly what the demo
prints, and a test fails if the two drift apart. No network, no external service, under a
second. pip install ctrlrun && ctrlrun demo runs it locally in about the same time.
Where it stops. It does not detect prompt injection: it contains the consequence rather
than reading the cause. It cannot promise exactly-once against a remote it does not control, it
refuses to knowingly act twice, and it rolls nothing back. Receipts are chained, so an alteration
is detected; a truncation at the end and a forged append are not, because the head that would catch
them is a row in the same database, and closing that is what ctrlrun anchor is for. They are not
signed: alteration is not authorship. The badge above means the
declared guarantees pass in the setup they ran against, and it does not mean secure, safe,
compliant, certified or audited:
what the badge means
· OWASP-AGENTIC-TOP10.md
names the four entries this does not address.
If an agent only reads and answers, you do not need CTRLRun. The moment it can send, pay, refund, delete, deploy, grant, revoke, approve, submit, purchase or cancel, you do.
Related MCP server: legitify-mcp
Use it in three steps
The animation above is this section, recorded against the real library: one policy file, two short programs, four commands, nothing staged.
1. Install it.
pip install ctrlrun2. Write down what the agent may do. One file, ctrlrun.yaml. Amounts are integer minor
units, so 50000 is €500. Both ends of every band are bound, because an upper bound alone lets
a negative amount through, and a refund of a negative amount is a charge. Anything not listed is
denied; there is no default-allow.
schema: ctrlrun.policy/v2
actions:
stripe.refund:
effect: "refund:{payment_id}"
rules:
- when: { amount_gte: 0, amount_lte: 50000 }
decision: allow # up to €500: the agent acts alone
- when: { amount_gte: 0, amount_lte: 1000000 }
decision: approve # up to €10,000: a human decides
- decision: deny # above that: never3. Wrap the call that has the consequence. The decorator names the action, the effect key
names the consequence it has in the world, and the context names who is acting. stripe here
is a stand-in that records calls instead of making them.
import sys
import ctrlrun
class FakeStripe:
"""Stands in for the provider: it records calls instead of making them."""
def __init__(self) -> None:
self.calls: list[tuple[str, int]] = []
def refund(self, payment_id: str, amount: int) -> dict:
self.calls.append((payment_id, amount))
return {"id": f"re_{payment_id}", "amount": amount, "status": "succeeded"}
stripe = FakeStripe()
@ctrlrun.protect("stripe.refund", effect="refund:{payment_id}")
def refund(payment_id: str, amount: int) -> dict:
return stripe.refund(payment_id, amount)
if __name__ == "__main__":
with ctrlrun.context(agent="support-agent"):
print("€500 ->", refund(payment_id="txn_1", amount=50_000)["status"])
try:
refund(payment_id="txn_2", amount=500_000)
except ctrlrun.ApprovalRequired as pending:
print("€5,000 -> a human decides:", pending.request_id)
with open("request_id.txt", "w") as handle:
handle.write(pending.request_id)
else:
sys.exit("the €5,000 refund ran without a human; the policy is not in force")
print("calls that reached the provider:", len(stripe.calls))The €500 refund runs on its own. The €5,000 one stops and names the request a human answers:
€500 -> succeeded
€5,000 -> a human decides: apr_63e80076f2cccfee52b17491a4b2e125
calls that reached the provider: 1A human answers from the shell. The grant names the hash of the exact action the human read, and when it lapses. Ids, hashes and dates are generated per run; yours differ.
ctrlrun approve "$(cat request_id.txt)"granted apr_63e80076f2cccfee52b17491a4b2e125 for sha256:22ec1c398e4b93d080b6cba61e5e11b0e21879552ac5dbf63c192d2b2e6af752
expires 2026-09-13T20:10:11.367ZThe agent presents it, then tries to spend it on something else. The first call is exactly what the human approved, and it runs. The second is the same approval with one digit changed, and it matches nothing:
import sys
import ctrlrun
from agent import refund, stripe
with open("request_id.txt") as handle:
request_id = handle.read().strip()
with ctrlrun.context(agent="support-agent"), ctrlrun.with_approval(request_id):
# Exactly what the human read: €5,000 on txn_2.
print("€5,000 with the approval ->", refund(payment_id="txn_2", amount=500_000)["status"])
# The same approval, one digit changed.
try:
refund(payment_id="txn_2", amount=900_000)
except ctrlrun.ApprovalMismatch:
print("€9,000 on that same approval -> refused")
else:
sys.exit("a mutated action ran on a human's approval; that is the bug this exists to stop")
print("calls that reached the provider:", len(stripe.calls), "(the €9,000 never left)")€5,000 with the approval -> succeeded
€9,000 on that same approval -> refused
calls that reached the provider: 1 (the €9,000 never left)Every attempt, refusals included, left a receipt, and ctrlrun receipts lists them. That is the
whole integration: a policy file, a decorator, a context, and with_approval to present a grant.
Money is the example, not the scope. A condition is <argument>_<op>, so the same policy
language reads role_in: [reader, viewer] or replicas_lte: 10 as easily as amount_lte, and
nine domains below have one policy each.
Protect your first action walks the same path
with every output explained ·
Policy YAML reference ·
Cookbook: refunds, deploys, IAM, deletions, email, MCP.
Three ways to use it
You probably do not need an adapter. @protect covers anything running in this process: a
raw model call, a LangChain tool, a hand-rolled loop, a cron job. The gateway covers anything
that reaches its tools over MCP, in any language.
You have | Use | Needs |
Python in this process | the | nothing beyond |
Tools behind an MCP server, in any language | the gateway: | one command, no change to agent or server code |
A framework with its own approval interrupt | an adapter | the framework to have a human-in-the-loop primitive |
It works with agents you can and can't modify. WhatsApp, Slack and Teams bots, ChatGPT, Cursor, Codex, OpenAI Agents: any AI agent you have. CTRLRun checks the action, not the agent, so if the agent acts through a tool server or an API you run, the action is checked, and the agent is not rebuilt, redeployed or told. Agents you can't modify says where the boundary goes for each kind.
An adapter exists for one reason: to route an approve decision through the framework's own
interrupt, so a human answers where they already answer. There is never a second place to say
yes. ctrlrun-langgraph
gives prevention, because the resumption carries the arguments and core re-checks them
against the hash.
ctrlrun-openai-agents
gives attribution, because that SDK records that a call was approved and not what its
arguments were. None of the three is only for agents: a worker, a webhook handler and a
scheduled job cannot tell a first attempt from a retry either.
How it works
Every protected call, whichever way it arrives, goes through the same seven steps. Only then does it reach your systems.
normalize → decide → approve → reserve → execute → resolve → recordNormalize: one action, one id. The call becomes an
Action: a name, canonical arguments (sorted keys, no floats), a resource, the principal. Its SHA-256 is the action hash.Decide: allow, ask or block. Authority first (may this principal propose this at all, and within what bounds?), then policy (how much autonomy does this action get?). Unknown action, missing policy or missing principal is
deny. Silence is never permission.Approve: bound to this action. A human answers against the action hash. The approval is single-use, expires, and matches nothing but that exact action, so arguments changed after the answer void it and a person answers again. Name a
preconditions=provider and the approval is also bound to the resource state it was granted against, rechecked strictly before the reservation: that narrows the window between the answer and the execution, from minutes of deliberation to milliseconds. It does not close it, because the recheck is a network call and cannot run inside the atomic write.Reserve: claimed once. The effect key,
refund:txn_1ornamespace:prod-eu:checkout, is taken in one atomic write. A second caller, in another process or on another host, is refused.Execute: your code runs. Only
NotExecuted, raised by you, meansFAILED; every other exception and every timeout meansAMBIGUOUS. Deciding which one you are looking at is the hard part, soctrlrun.transportdoes it for you:urlopen,HTTPConnectionandHTTPSConnectionfrom stdliburllibandhttp.client, which raiseNotExecutedonly where the connection they opened was handed no request byte. After one byte, every failure stays the exception it was, and the outcome isAMBIGUOUS. No setting widens that.Resolve: unknown is not failed. An
AMBIGUOUSeffect keeps its key and refuses a retry untilctrlrun resolve, or areconcilehook that asked the remote, says what happened. Nothing runs twice on a guess.Record: a receipt either way. A portable JSON receipt: who, what, decision, approval, effect key, outcome, and the hash of the policy that decided it, chained to the receipt before it. Refusals get one too.
Who may ask, and how much. The policy decides the action and cannot see who is asking. Who
may ask at all is a second axis, authority: every principal needs a grant, a delegation cannot
widen one, and an action needs both axes, the stricter of the pair. Since 0.9 a grant can also
carry a budget, a metric with a limit over a rolling window, consumed on reserve inside the
same write, so a thousand refunds that each pass amount_lte cannot add up to more than the
grant allows. An AMBIGUOUS effect holds its budget until it is resolved, because otherwise an
agent that can manufacture ambiguity could manufacture authority. A budget bounds what the next
reservation may do; it cannot recall an action already in flight.
Authority has the whole model.
State lives in SQLite by default, a file with no server and no ops, and the reservation holds
across processes rather than merely across threads. Point it at Postgres when more than one
host writes: pip install "ctrlrun[postgres]", one URL, the same guarantees graded by the same
suite. Prove it in your own setup with ctrlrun verify, which runs the kernel's own failure
scenarios against your policy in a scratch store. It reaches no network: the only sockets it
opens are to the store you named and to loopback listeners it bound itself, which is how it
grades the transport classifier.
Guarantee |
| Gateway | Adapter |
Approval binding — An approval is bound to the exact action; a mutated or replayed one is refused. | yes | yes | prevention or attribution, per adapter |
One effect, once — One logical effect happens at most once, across threads, processes and hosts. | yes | yes | yes |
Unknown is not failed — An unknown outcome is AMBIGUOUS, never FAILED, and blocks a blind retry. | yes | yes | yes |
Fail closed — An unknown action, a missing policy or a missing principal is denied. | yes | yes | yes |
Authority and delegation — Every principal needs a grant, delegation cannot widen one, and a grant bounds the total. | yes | yes | yes |
Receipts — Every executed action leaves a portable JSON receipt of who, what and outcome. | yes | yes | yes |
The same shape in nine domains
Nothing in CTRLRun knows what a refund is. An action is a name, canonical arguments, an effect key and a resource, and the three questions asked of it are the same whichever domain it came from: how much autonomy does this action get, did a human approve this exact action, and has this effect already happened. Two things carry your domain, and you write both.
The effect key is the only domain knowledge in the system. It is the string that says two calls are the same real-world consequence:
refund:{payment_id},namespace:{cluster}:{name},grant:{user_id}:{role},prescription:{patient_id}:{drug}. Name it well and a retry cannot act twice; leave it out and there is nothing for at most once to be about.Conditions are arguments, not amounts. The language is
<argument>_<op>, so the same operators readreplicas_lte: 10,role_in: [reader, viewer]andto_domain_eq: acme.comas easily asamount_lte. A band is available to a domain that has never issued an invoice.
Domain | Autonomous | A human decides | Never |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
|
Read any row left to right and it is one rule wearing different nouns. The security row is the one to read twice: adding a deny rule to a firewall is autonomous and adding an allow rule is not, which no amount threshold would have told you. The policy is where your judgement about your domain gets written down; CTRLRun is what makes it hold.
Documentation
docs.ctrlrun.dev is the documentation: concepts, guides, a cookbook, the full reference.
Start here | |
The ideas, and doing something with them | |
Agents and MCP | Agents you can't modify · MCP overview · The gateway in five minutes |
Running it for real | Production · Postgres · Recovery · Operations |
Every key, flag and error | |
Compared with | Idempotency keys · Framework human-in-the-loop · Guardrail libraries · Durable workflows · Governance toolkits |
What holds, and what does not | Threat model · What |
Contributing
Issues and pull requests are welcome:
CONTRIBUTING.md and
CODE_OF_CONDUCT.md have the
working agreement, and
SECURITY.md is how to report a
vulnerability. Every claim in this file has a test behind it, so a change to the prose usually
means a change to the suite.
CHANGELOG.md and
https://docs.ctrlrun.dev/ROADMAP say
where it is going. Releases carry PyPI provenance attestations from GitHub Actions.
License
Apache-2.0. The enforcement kernel is and will remain fully open source.
Available Tools
8 toolsapproveA
WRITES. Grants one pending approval request, letting the agent run that exact action once. Requires an authenticated human; the answer is recorded under their name and is visible in the receipt the action leaves.
| Name | Required | Description | Default |
|---|---|---|---|
| request_id | Yes | The pending request to grant, as returned by list_pending_approvals. The grant is bound to the hash of the action that request already names, so there is no argument here by which a different action could be approved. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden. It discloses that the operation writes, requires an authenticated human, records the answer under the human's name, and leaves a visible receipt. This is strong context, though it does not cover edge cases like duplicate or invalid requests.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two focused sentences with no filler. The key action, one-time nature, and human-authorization requirement are all front-loaded and every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter tool with no output schema, the description covers what the tool does, its prerequisite, its audit trail, and where the result is visible. An agent has enough context to invoke it correctly and anticipate the side effects.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the request_id parameter is already well-described in the schema, including its binding to the pre-approved action. The description adds no parameter-specific meaning, so the baseline score of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb and resource: it 'Grants one pending approval request' and explains the consequence (the agent may run that exact action once). This clearly distinguishes it from sibling tools like deny and list_pending_approvals.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies when to use the tool: when a pending approval request should be granted. It gives a prerequisite (authenticated human) but does not explicitly contrast with deny or other alternatives, so the usage guidance is mostly inferred rather than stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
denyA
WRITES. Refuses one pending approval request. Requires an authenticated human; the answer is recorded under their name.
| Name | Required | Description | Default |
|---|---|---|---|
| request_id | Yes | The pending request to refuse, as returned by list_pending_approvals. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations exist, so the description itself must disclose behavior. It does flag 'WRITES' and states that the answer is recorded under the authenticated human's name, which is important context. However, it stops short of explaining the result or implications of the denial, such as finality or effect on the request state.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Extremely concise: three short clauses that cover the write intent, the action, and a key precondition/audit requirement. Every sentence carries informational weight, with no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With a single parameter and no output schema, a super-rich description is not expected, but the description does not explain what the tool returns or what happens if the request is invalid or already processed. For a mutation tool with zero annotations, this is a notable gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema has 100% coverage for the request_id parameter, as it already says 'The pending request to refuse, as returned by list_pending_approvals.' The description adds little beyond that, so the baseline 3 applies; the pointer is useful path does not introduce substantial new semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly specifies a distinct action ('Refuses one pending approval request') with a specific verb and resource, and it naturally distinguishes from siblings like 'approve'. No ambiguity about what operation the tool performs.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives clear contextual guidance: it applies to pending approval requests and notes a mandatory authenticated human. It does not explicitly state when to prefer this over 'approve' or 'resolve', but the opposite action is implied by the sibling naming.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
effectsA
Read-only. The logical effects this store knows about, one row per effect key. Use this to find the effects whose outcome is unknown, which are the ones resolve can move on and the ones blocking a retry.
| Name | Required | Description | Default |
|---|---|---|---|
| state | No | Return only effects in this state, or omit it for every effect. 'reserved' and 'executing' are in flight, 'committed' and 'failed' are settled, and 'ambiguous' is the one that needs a human: the effect may or may not have happened, and a retry stays refused until resolve says which. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden. It openly declares 'Read-only' at the start, which is a key behavioral trait and sufficient for a simple query tool. It does not detail pagination or other edge behaviors, but for a read-only list tool this level is adequate.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences with no filler. The first sentence front-loads the read-only nature and scope; the second gives a precise use case. Every word earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-optional-param read tool, the description covers what it does and when to use it, and the schema covers the param semantics. With no output schema, an agent may not know the exact return fields beyond 'one row per effect key', but the simple nature of the tool makes this acceptable.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, with the single 'state' parameter thoroughly described including enum meanings. The tool description itself adds little parameter-specific semantics beyond the schema, so the baseline score of 3 applies. It only hints at which states matter for the resolve/retry workflow.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the resource (logical effects) and verb (list/read) with a specific granularity ('one row per effect key'). It avoids tautology and conveys a concrete purpose, though it does not explicitly differentiate from sibling tools like stats or inspect_action.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives an explicit use case: 'Use this to find the effects whose outcome is unknown, which are the ones resolve can move on and the ones blocking a retry.' This tells the agent when to use it, but does not mention when not to use it or point to alternatives, so it stops short of a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
inspect_actionA
Read-only. One action's whole history: what was proposed, what the policy decided, which approval was involved, what happened to the effect, and the receipt. Use this when you know which action you care about; use receipts to browse, stats for totals.
| Name | Required | Description | Default |
|---|---|---|---|
| action_id | Yes | The action to inspect. Action ids appear on receipts and on the pending requests list_pending_approvals returns. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the behavioral disclosure burden. It leads with 'Read-only,' a clear safety signal, and details what the call returns in terms of history components. It does not mention failure modes or permissions, but for a simple read operation the key behavioral trait is disclosed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two well-formed sentences with no wasted words. The most critical behavioral fact ('Read-only') is front-loaded, followed by the resource detail and then usage guidance. Every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter, read-only inspection tool with no output schema, the description provides enough detail about what will be returned and when to use it. It lacks error-handling notes, but the tool is simple enough that the description is complete for correct selection and invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the schema already explains where action_ids appear (receipts and list_pending_approvals). The description adds only the context of when to use it, not additional parameter semantics, so baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Description clearly states a specific verb ('inspect') and resource ('one action's whole history') and enumerates exactly what is covered: proposal, policy decision, approval, effect outcome, and receipt. It also distinguishes itself from sibling tools by explicitly naming receipts and stats as alternatives for browsing and totals.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit when-to-use rule: 'Use this when you know which action you care about' and contrasts it with 'use receipts to browse, stats for totals.' This is concrete and directly actionable, leaving little ambiguity about which tool to select.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_pending_approvalsA
Read-only. The approval requests waiting for a human, oldest first, with the action, its arguments and when the request expires. Start here: the request ids approve and deny take come from this list.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | How many pending requests to return, 1 to 200, default 50. This bounds the response and not the scan, so a store holding many answered requests still walks them to find the pending ones. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden of behavioral disclosure. It clearly states 'Read-only', describes what the list contains, and specifies ordering. It could add more about pagination or empty results, but for a simple read-only list it is transparent enough.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences with no filler. It front-loads the read-only nature, states the core behavior, and closes with the workflow-relevant instruction. Every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a list tool with one optional parameter and no output schema, the description is complete enough: it names what is returned, the ordering, the read-only guarantee, and how the result relates to sibling tools. An agent has everything needed to call it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, and the limit parameter already has a detailed description in the schema. The tool description adds no parameter-specific meaning beyond that, so the baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Description names a specific verb and resource: listing pending approval requests. It clarifies the scope ('waiting for a human'), ordering (oldest first), and the included contents, making it easy to distinguish from sibling tools like approve and deny.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The instruction 'Start here' explicitly tells the agent to use this tool before approve and deny, and explains that the request IDs for those tools come from this list. This is direct workflow guidance that prevents misuse.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
receiptsA
Read-only. The individual receipts this store holds, as portable JSON, oldest last, one for every attempt including the refusals. Use this to read what happened; use stats for totals, and inspect_action for one action end to end.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | How many receipts to return, 1 to 200, default 20, oldest last. | |
| control | No | Return only receipts citing this control id, exactly as `ctrlrun receipts --control` filters. Omit it to return every receipt. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description discloses that the tool is read-only, returns receipts oldest last, includes refusals, and returns portable JSON. Since no annotations are provided, the description carries the full burden, and it does so well. It doesn't mention pagination or rate limits, but the limit parameter is documented in the schema, so the core behavioral traits are covered.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the most important fact (read-only), and every clause earns its place. The sibling routing is compact and clear. No wasted words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only list tool with two optional parameters and no output schema, the description covers the essential context: what it returns, ordering, inclusion of refusals, and how to route to siblings. It doesn't describe the exact JSON shape, but the description says 'portable JSON' and the schema covers parameters, so the missing return format is a minor gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents both parameters. The description adds context by explaining the 'control' parameter's filtering behavior ('exactly as `ctrlrun receipts --control` filters') and the ordering ('oldest last'), which enriches the schema's bare descriptions. This exceeds the baseline 3.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('read'), a resource ('individual receipts'), and key characteristics ('portable JSON, oldest last, one for every attempt including refusals'). It also distinguishes itself from siblings by naming stats and inspect_action as alternatives. This is a clear, specific purpose that an agent can act on.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly says when to use this tool ('Use this to read what happened') and when to use alternatives ('use stats for totals, and inspect_action for one action end to end'). This is direct routing guidance that leaves no ambiguity about selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
resolveA
WRITES. States what actually happened to an effect whose outcome is unknown. Requires an authenticated human and a reason; the answer is recorded under that person's name, and a 'failed' resolution permits a retry that is currently blocked.
| Name | Required | Description | Default |
|---|---|---|---|
| reason | Yes | How you know, in plain words. Must be non-empty. A resolution answered through an assistant has a conversation behind it and no record of it, so this reason is that record. | |
| outcome | Yes | What you established actually happened at the remote. 'committed' means the effect took hold and must never run again; 'failed' means it did not, and is what releases the blocked retry. Check the remote before answering: this is a claim about the world, not a guess. | |
| effect_key | Yes | The effect to settle, as it appears in the effects list, for example refund:txn_1. Only an effect whose outcome is unknown can be resolved. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden and does so well. It opens with 'WRITES,' discloses the authentication requirement, states that the answer is recorded under the person's name, and explains the retry-unblocking consequence of a failed resolution.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact and front-loaded, beginning with the mutation warning and then providing the essential conditions and consequences in two sentences. Every clause earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers prerequisites, side effects, and the core purpose well, and the schema fills in parameter details. It does not describe the expected return value or error behavior, and since there is no output schema, a fully complete description would mention what the caller gets back.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema already provides detailed descriptions for all three parameters, so schema coverage is 100%. The description adds behavioral context but no parameter-level meaning beyond what the schema already offers.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states a specific action and resource: it 'States what actually happened to an effect whose outcome is unknown.' It is easy to distinguish from inspection or listing tools, though it does not explicitly differentiate itself from sibling tools like approve or deny.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives clear usage context: resolve only an effect whose outcome is unknown, and do so with an authenticated human and a reason. It stops short of saying when not to use the tool or which sibling tool should be used instead.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
statsA
Read-only. One summary of what this store's receipts say over an optional window: how many actions were allowed, refused or sent to a human, and how their effects ended. Use this for totals; use receipts when you need the records themselves.
| Name | Required | Description | Default |
|---|---|---|---|
| since | No | How far back to count: an ISO-8601 timestamp carrying an offset, or a relative window written as <n>m, <n>h or <n>d, such as 30m, 24h or 7d. Omit it to count every receipt in the store. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden of behavioral disclosure. It declares the operation is read-only, explains that it returns aggregated counts rather than records, and names the categories included (allowed, refused, sent to human, effect outcomes). It doesn't detail output structure, but for an aggregation tool this is adequate.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences convey the tool's core behavior, scope, and routing guidance without waste. The read-only and summary aspects are front-loaded, making the tool's identity immediately clear.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with one optional parameter and no output schema, the description covers what the tool does, what categories it counts, and when to use it versus the receipts alternative. It lacks a description of the exact output fields, but the summary nature and listed categories give an agent sufficient expectation of the return value.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage for the single optional parameter 'since' is 100%, with a detailed description of formats and omission behavior. The tool description only references an 'optional window,' adding no new information beyond what the schema already provides, so the baseline of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: it produces a read-only summary of receipts over an optional window, counting allowed, refused, and human-sent actions and their outcomes. It distinguishes itself from the receipts tool by explicitly saying this tool provides totals rather than individual records.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit direction: 'Use this for totals; use receipts when you need the records themselves.' This tells an agent exactly when to choose this tool over its siblings, with no inference required.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
8 tool updates
- Changed
approve1 field changed- added
Input schema / properties / request_id / descriptionAdded value: +"The pending request to grant, as returned by list_pending_approvals. The grant is bound to the hash of the action that request already names, so there is no argument here by which a different action could be approved."
- Changed
deny1 field changed- added
Input schema / properties / request_id / descriptionAdded value: +"The pending request to refuse, as returned by list_pending_approvals."
- Changed
effects1 field changed- added
Input schema / properties / state / descriptionAdded value: +"Return only effects in this state, or omit it for every effect. 'reserved' and 'executing' are in flight, 'committed' and 'failed' are settled, and 'ambiguous' is the one that needs a human: the effect may or may not have happened, and a retry stays refused until resolve says which."
- Changed
inspect_action1 field changed- added
Input schema / properties / action_id / descriptionAdded value: +"The action to inspect. Action ids appear on receipts and on the pending requests list_pending_approvals returns."
- Changed
list_pending_approvals1 field changed- added
Input schema / properties / limit / descriptionAdded value: +"How many pending requests to return, 1 to 200, default 50. This bounds the response and not the scan, so a store holding many answered requests still walks them to find the pending ones."
- Changed
receipts2 fields changed- added
Input schema / properties / control / descriptionAdded value: +"Return only receipts citing this control id, exactly as `ctrlrun receipts --control` filters. Omit it to return every receipt." - added
Input schema / properties / limit / descriptionAdded value: +"How many receipts to return, 1 to 200, default 20, oldest last."
- Changed
resolve3 fields changed- added
Input schema / properties / effect_key / descriptionAdded value: +"The effect to settle, as it appears in the effects list, for example refund:txn_1. Only an effect whose outcome is unknown can be resolved." - added
Input schema / properties / outcome / descriptionAdded value: +"What you established actually happened at the remote. 'committed' means the effect took hold and must never run again; 'failed' means it did not, and is what releases the blocked retry. Check the remote before answering: this is a claim about the world, not a guess." - added
Input schema / properties / reason / descriptionAdded value: +"How you know, in plain words. Must be non-empty. A resolution answered through an assistant has a conversation behind it and no record of it, so this reason is that record."
- Changed
stats1 field changed- added
Input schema / properties / since / descriptionAdded value: +"How far back to count: an ISO-8601 timestamp carrying an offset, or a relative window written as <n>m, <n>h or <n>d, such as 30m, 24h or 7d. Omit it to count every receipt in the store."
8 tool updates
v0.12.1- First observed
approve - First observed
deny - First observed
effects - First observed
inspect_action - First observed
list_pending_approvals - First observed
receipts - First observed
resolve - First observed
stats
TDQS
Scored across 8 tools
Each tool maps to a distinct resource or workflow step: pending list, approval/denial mutations, action history, raw receipts, effect states, aggregate stats, and effect resolution. No two tools are interchangeable, and the descriptions explicitly direct agents to the right one.
Names split between bare verbs (approve, deny, resolve), verb-noun read commands (list_pending_approvals, inspect_action), and bare plural nouns for data views (receipts, effects, stats). They are readable and meaningful, but the naming convention is not uniformly predictable.
Eight tools fit the approval-console domain well: write operations are covered by approve, deny, and resolve, while read-side views cover pending items, per-action history, raw receipts, effects, and statistics. No tool feels redundant or missing as filler.
The tool set covers the full workflow: identify pending requests, decide them, inspect full action history, inspect effect outcomes, and record resolutions that unblock retries. Auditing is supported through receipts and stats, so there are no obvious dead ends.
Maintenance
Related MCP Connectors
Human-in-the-loop approval for agent actions, with verifiable action-bound receipts.
Runtime permission, approval, and audit layer for AI agent tool execution.
Supervised API-write gateway for AI agents with policy, human approval and execution receipts.
Human approvals, notifications, inbound webhooks, wake-ups for headless agents. Free trial: /try
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceProvides a human approval gate for AI agents, enabling interactive inline cards for approving, editing, or rejecting actions before they are executed.MIT

legitify-mcpofficial
AlicenseAqualityDmaintenanceEnables AI agents to submit, check, and manage human attestation requests for AI actions through Legitify's approval receipt system.6MIT- AlicenseAqualityCmaintenanceProvides a human-approval tool for MCP-compatible agents, pausing execution until a human approves or edits the action, with every decision logged.22 npm2MIT
- AlicenseNot gradedqualityBmaintenanceGates agent tool execution with human approval, audit trails, and replay-resistant permits, enabling safe use of tools in agent loops.MIT