DEV Community

Sean McDonnell
Sean McDonnell

Posted on Edited on Fully Autonomous

Put a permission gate between your agent and its tools

An agent can understand a task and still attempt an action you did not authorize.
The useful place to decide whether that action may run is the dispatcher that
actually performs it. A prompt can explain the rules; a dispatcher can enforce
them before a connected tool body starts.

I built DeadBolt at NextEleven to provide that local admission point. It is free,
open source under Apache-2.0, and usable as an embedded Rust library, local HTTP
sidecar, or proxy in front of a trusted stdio MCP server. This walkthrough uses
the MCP route, with disposable marker tools instead of real accounts.

Authorship disclosure: this article was drafted by an AI coding assistant.
The implementation and instructions were checked against the real published
bundle and disposable tool effects. Those checks do not establish that every
agent, client or deployment is protected.

Update — October 12, 2026: stable 2.0.0 is published, with its matching editor guide and helper. It includes fixed run deadlines, optional supervisor-only liveness, separated operator/workload credentials, exact-action approval and readable MCP denials. Conflicting recognized destinations are refused, and a damaged existing store cannot be silently rebuilt at startup. The worked tutorial below remains historical and pinned to bundle 1.1.0 / engine 1.0.3; use the current guide for 2.0.0 and do not mix helpers or configurations. Stable artifacts passed installed/protocol/effect checks across four native platforms. Earlier RC3 GUI checks are separately recorded; they are not certification or proof of coverage in your application.

We are keeping 2.0.0 as the release baseline through Monday, October 19, collecting feedback and reviewing it weekly. The next iteration will focus on reproducible bugs, documentation and integration friction, with checks before publication. DeadBolt is Apache-2.0: you may use, fork, modify and integrate it, including commercially. Useful adapters and improvements are welcome for review and possible inclusion; further expansion will follow demonstrated user needs.

Choose one route to protect

This is a specific integration boundary. The proxy does not intercept built-in
terminal commands, file editing, other MCP connections or arbitrary network
traffic. The child server retains its operating-system permissions and must be
trusted. Initialization and non-tool MCP methods are outside the admission gate.

For this example, the path is:

Editor / MCP client
        ↓ tools/call
DeadBolt admission check ← trusted operator approval / kill
        ↓ only after admission
Trusted disposable MCP server
        ↓
Local marker file
Enter fullscreen mode Exit fullscreen mode

The editor can request an action. The operator owns policy and approval. DeadBolt
checks the routed request before forwarding it. The marker file makes it possible
to distinguish a denied response from a tool that ran anyway.

Start without a model account

Download DeadBolt MCP bundle 1.1.0
and the configuration helper.
Review the helper, save both in Downloads, and use Python 3.9+ with Node.js 20+.
The bundle supports Apple Silicon/Intel Macs, Linux x64 with glibc and Windows x64.

From Downloads on macOS/Linux:

OUT="$HOME/.local/share/deadbolt/editor-1.1.0"
python3 editor_setup.py prepare --bundle deadbolt-1.1.0.mcpb --out "$OUT"
python3 editor_setup.py verify --out "$OUT"
Enter fullscreen mode Exit fullscreen mode

Windows users can follow the PowerShell equivalents in
the editor installation guide.
Use a new private directory outside any agent-editable workspace.

The helper verifies the full bundle's published checksum, refuses to overwrite
an installation, and exports configuration without modifying existing profiles.
The verification starts a disposable server, tests real calls and removes its
temporary state. It needs no model key, hosted operator endpoint or connected
account. The bundle is version 1.1.0; its engine is stable 1.0.3.

Expected results include:

{"case":"allowed_marker","passed":true,"body_ran":true}
{"case":"purpose_exceeded","passed":true,"body_ran":false}
{"case":"needs_human","passed":true,"body_ran":false}
{"case":"killed","passed":true,"body_ran":false}
Enter fullscreen mode Exit fullscreen mode

The complete report also verifies one approved execution and that its approval
is consumed. This is a protocol test with local effects, not a benchmark of a
model's judgment or proof of enforcement in your application.

Add it to an editor

For Cursor, open the generated install.html and review the local command in its
installation prompt. For VS Code, merge the exported vscode-mcp.json server
entry into the configuration opened by MCP: Open User Configuration. Preserve
existing servers. A new portable workspace configuration is also exported.

Initially, only two read-only setup tools are available: an integration guide and
a checksum check. Connected setup is not active protection. It starts no
downstream server. Use this mode to check the runtime before wiring an action.
An editor's AI chat may need its own account even though the local protocol test
does not.

See denial, approval and revocation

Create a fresh disposable demo with:

python3 editor_setup.py demo --out "$OUT"
Enter fullscreen mode Exit fullscreen mode

Use the returned directory's install link or configuration to add deadbolt-demo.
Its three tools append their names to a fixed local file. Set DEMO to the
returned directory path. There are no real emails, payments or account actions.

  1. Call demo_write_marker({}). The allow-listed action adds one line.
  2. Call demo_off_list({}). The policy blocks it with purpose_exceeded; no line is added.
  3. Call demo_publish_marker({}). It is deliberately classified as irreversible for the demonstration and returns needs_human; no line is added.
  4. In your trusted operator terminal, run:

    python3 editor_setup.py operator --out "$OUT" --demo "$DEMO" approve
    

    Call demo_publish_marker({}) once. It writes a line. A second attempt needs
    approval again.

  5. Revoke the run from the operator terminal:

    python3 editor_setup.py operator --out "$OUT" --demo "$DEMO" kill
    

    The next marker call returns killed and leaves the file unchanged.

Inspect the demo's effects.txt and events.jsonl. A tool's error response is
useful; evidence that its body did not run is the stronger check. An editor's
Allow/Run button permits an attempted call and does not grant operator approval.

The default lease expires after 60 seconds without admission. If your setup or
conversation takes longer, remove/stop the old demo connection and create a fresh
one. Admission renews a live lease; reconnecting cannot revive an expired or
killed ID. The automated verification handles this demonstration without a manual
conversation delay.

Carry the boundary into your own application

Replace a trusted downstream server's direct launch with the DeadBolt proxy.
Keep policy, approval and revocation in a separate trusted operator path. Remove
the ungated direct connection, configure actual tool/destination conventions,
and repeat the effect checks against one bounded action in your executor.

Stable 1.0.3 approvals apply once to an agent/tool pair; they are not bound to
exact arguments or recipients. Stable 2.0.0 includes
exact-action APIs and their limits. Do not assume those features are in this older bundle; the MCP bundle’s ordinary one-shot approval does not provision exact-action grants.

DeadBolt does not cancel an action already running, make an external action atomic
with admission, or protect routes you have not connected. Same-user malicious code
can also reach same-user state: private folders are useful hygiene, not an OS
sandbox. A deployment's executor and operator separation must be trustworthy.

The next useful test is small: one real route, an allowed action, a denied action
whose body stays untouched, and a revoked run that cannot perform the next effect.
Measure installation time and what confused the developer before widening scope.

Source and installation guide
· Captioned overview and full walkthrough
· Integration boundaries

If you try it, which step took the most work: installation, choosing the routed
tools, or keeping operator authority separate from the agent?

Top comments (7)

Collapse
 
autenai profile image
Auten •

The split between "the prompt explains the rules" and "the dispatcher enforces them" is clear, and showing that the tool body did not run is a stronger test than reading an error message.

I work on Auten, an MCP server that gives an AI agent a real screen to drive (a computer, and an Android phone in beta). For that kind of server, the point mickyarun makes about payment amounts gets one step harder: the risky action is usually a generic click or type call, and whether it buys, deletes or sends is not in the arguments at all. It depends on what is on screen at that moment.

Your rc.2 exact-action fingerprint binds the complete JSON arguments, which fits payments well. For a click on an element id, though, identical arguments can mean "Pay" one minute and "Cancel" the next. Would you expect the server to declare intent with each call (for example, "click the element labelled Pay") so the operator reviews something meaningful, or does that belong in a trusted adapter like the one you describe for payment vendors?

Collapse
 
seaneboneslang profile image
Sean McDonnell • • Edited

Thanks, Auten. That belongs in the trusted adapter/executor. A model-supplied intent such as ‘click Pay’ can help explain a request, but it cannot establish what the click will actually do.

You are right: hashing identical click arguments does not bind changing screen semantics. The adapter would need to resolve the target from a fresh UI observation, include the relevant target/context and effect-affecting values in the reviewed request, and revalidate them immediately before execution. If the target or context changed, or cannot be established reliably, it should refuse and require a fresh review. That only works if the trusted executor checks and executes the reviewed snapshot; adding a label to JSON alone provides no such guarantee.

DeadBolt supplies the admission/grant check, not a screen interpreter or an atomic transaction with the UI. There can still be a race between validation and a GUI click. For payments or similarly consequential effects, I would prefer a validated application API with idempotency and outcome reconciliation where available, and keep the final commit separate from ordinary navigation.

The exact-action contract is now in stable 2.0.0: review/execution requirements and limits. The article’s worked example is still explicitly the older bundle; I’m updating its current-release notice. We do not currently ship a generic GUI adapter.

For context, stable 2.0.0 is now published. It brings together fixed run deadlines, optional supervisor-only liveness, operator/workload credential separation, exact-action approval, and readable MCP denial reasons. The proxy rejects conflicting recognized destinations, and startup refuses a damaged existing store rather than silently rebuilding it. These controls apply to the routes wired through the trusted gate; they do not sandbox the whole agent or cancel an effect already running.

We will keep 2.0.0 as the release baseline through next Monday, October 19, collect developer feedback during the week, and review it weekly. The next iteration will focus on reproducible bugs, clearer documentation and integration friction, with validation before publication. We are holding further expansion until real usage shows what is needed.

DeadBolt is free and open source under Apache-2.0. You are welcome to use it, fork it, modify it and build an Auten-specific adapter under that license, including for commercial use. You do not need to wait for our roadmap. A reproducible example or contribution would be welcome, though sharing changes back is optional. If you build an adapter or improvement that works well, we would be happy to review it for inclusion in DeadBolt, with tests and a clear explanation of its trust boundaries.

For Auten, do your element identifiers carry a screen/document revision, or can they be reused across UI changes? That seems like the first contract to establish before treating a click as a reviewed effect.

Collapse
 
autenai profile image
Auten •

Thanks, that is a clear line: a label from the model can explain a request, but only the executor can establish what the click does.

On ids: today they are plain numbers from the most recent list_elements read of the focused window, and the documented flow is list, then click(id). We don't publish a revision or staleness contract for them, so an adapter should not assume an id survives a UI change. It should treat the id as bound to the observation that produced it and re-read before acting, which is the fresh-observation and revalidate step you describe.

Keeping the final commit separate from ordinary navigation is close to what we ask of the model: the safety skill in the Auten plugin tells the agent to stop and ask before buy, delete, send or permission steps. That is a policy for the model, not an enforced gate, so it sits in a different layer from DeadBolt.

Congrats on 2.0.0.

Thread Thread
 
seaneboneslang profile image
Sean McDonnell • • Edited

Thanks, that answers the ID question, and I appreciate you being clear about what the safety skill does. Those are useful complementary layers: the skill helps the agent ask for review; a trusted executor has to enforce the decision.

For Auten, I would treat an ID as belonging only to the observation that produced it. A fresh read can detect changes, but it still cannot make a GUI click atomic with that read. If an adapter cannot establish that the reviewed target and effect still match, it should stop for a fresh review rather than reuse the approval. Adding a label to the arguments alone would not establish that guarantee.

That gives us a sensible place to start: one bounded operation, with allowed and denied effect checks, before claiming broader protection. DeadBolt 2.0.0 supplies the local admission and approval contract; it does not currently ship a generic GUI adapter.

Thanks for the congratulations, too. If you try an Auten-specific integration, a small reproducible example showing where the observation, approval and execution meet would be very welcome. We are collecting integration feedback against the stable release and reviewing it weekly.

We will try to implement every useful request we can. We will assess each one for practical value and fit, prioritize reproducible fixes and integration friction, and validate changes before release. If something needs a narrower scope or belongs in a separate adapter, we will explain that openly.

We also really appreciate the time you have spent reading, evaluating and discussing the tool. Specific feedback like this helps us make the implementation and its limits clearer for everyone using it.

Thread Thread
 
autenai profile image
Auten •

Agreed on all three points: an id only means something for the read that produced it, a fresh read narrows the gap but can't make the click atomic with it, and a label in the arguments proves nothing by itself. Stopping for a fresh review instead of reusing the approval is the honest default.

Nothing Auten-specific is in progress on our side, so no promises. If we do try it, a small reproducible example of where observation, approval and execution meet is exactly the form we'd share it in. Thanks for the careful answers.

Collapse
 
mickyarun profile image
arun rajkumar •

The admission point belongs at the dispatcher, and the limits section is the most useful part of the post, which is not how these usually go. Two things from putting a gate like this in front of money.

A tool-call gate cannot see the amount unless it parses the arguments, and the arguments belong to the vendor. payments.create for fifty pounds and payments.create for five thousand are the same tool and the same shape. So a policy written over tool names is a policy about capability, not about consequence, and the consequence is what an operator actually wants to approve. The moment the policy does start reading fields it inherits the vendor's schema: the field moves, the gate does not recognise it, and an unrecognised field is almost always allowed rather than refused. That is the same failure somebody wrote up here this morning about an allow-list that is default-deny at the verdict and default-allow at the selector. Worth deciding explicitly what DeadBolt does when a routed request carries a field no rule mentions, and saying so in the readme.

The second is the line most readers will skip. The proxy does not intercept built-in terminal commands, file editing or other MCP connections. For an editor-based agent that is most of the ways it can write. A gate in front of one of four doors is a real control over that door and it is not an admission point for the agent, and the difference matters because the first thing an operator does after installing one is stop watching the others. I would move that sentence above the diagram.

What I would want to know and the post does not say: when DeadBolt denies, what does the agent see. A refusal that comes back as a tool error gets retried, rephrased and routed around, which is a different outcome from a refusal that halts the run. In payments that distinction is the whole design, because an agent that can retry a declined payment a hundred times is a worse problem than the one you blocked.

Collapse
 
seaneboneslang profile image
Sean McDonnell •

Thanks, Arun. These are useful distinctions, especially for payments.

A tool-name allowlist approves a capability; it does not distinguish £50 from £5,000. The broad one-shot approval used in this article is also permission for the next agent/tool call, not approval of its exact amount or recipient. Extra vendor argument fields are not automatically rejected merely because the tool-name policy has no rule for them. That needs to be explicit.

The separately published 2.0.0-rc.2 preview has an opt-in exact-action path: an operator reviews a fingerprint binding the run, tool, destination, complete JSON argument object, nonce and deadline. Changed arguments cannot use the old grant. Unknown fields in DeadBolt’s own action envelope are rejected, but that is not validation of a payment vendor’s argument schema. The MCP bundle’s ordinary approval configuration does not provision these exact-action grants.

For a payment integration, I would require a trusted, versioned adapter to reject unknown or invalid vendor fields, resolve defaults and every value affecting the payment before review, and execute that same validated snapshot. Schema drift must stop dispatch rather than fall back to tool-name approval. Provider idempotency and outcome reconciliation are still needed; consuming a grant does not prove a payment completed. Exact-action contract and limits.

You are also right about the other doors. The coverage warning belongs above the diagram: this proxy protects the connected MCP route. Built-in terminal/file tools and other connections remain outside it. Installing it does not establish an agent-wide boundary.

On what the agent sees: RC2 returns a normal MCP tool result with isError: true, a stable reason such as killed or lease_expired, and a bounded retry hint. Terminal run states say “Do not retry this run”; pause requires operator resumption. Those newer semantics should not be inferred from the older bundle used in this walkthrough. RC2 bundle behavior.

That response blocks the attempted downstream call; it does not force the model to stop its loop or prevent it trying another route. A payment executor must enforce its stop/escalation behavior outside the model and keep every payment route behind the trusted dispatcher. A retry hint alone is not that control.

I appreciate the concrete review. It identifies exactly where capability approval, consequence approval and execution coverage need separate explanations.