Agent Guard and the permission to try again

AI agent security, part 4 of 4. Two tests returned the same error. One had made no change. The other had already written its receipt. We tested the approvals and evidence needed before an agent tries again.

Two of our Agent Guard tests returned TOOL_FAILED, although one had made no change and the other had already written its receipt. An agent deciding whether to try again would need more than that error.

Both tests used local synthetic state and the public 0.1.0-alpha.2 release. We used the embedded core for the first and the durable executor for the second. In part two, a read-only task tried to gain deployment access through two handoffs. Here the release tasks had the grant and approval to act. We wanted to check what stayed attached to that approval, and what evidence survived when the handler failed.

Agent Guard puts those checks in trusted application code. The host gives each task explicit permissions and keeps credentials, task creation and approval outside the model’s control. Delegation can retain or narrow grants, and calls count against the task ancestors’ budgets. These guarantees cover the task tree served by the same guard or executor. An independent receiving service must check the caller’s authority too. Task grants and delegation

The handoff also carries restrictions on data use. Once trusted host code marks a task family as sensitive, the guard blocks external sends from that family, including otherwise approved sends. Copying the data into a new task requires the host to preserve the relevant context. A summary does not become public just because a different agent wrote it. Sensitivity and trusted context

What exactly did we approve

An approval for a plan can be quite vague. “Fix the issue” could eventually mean changing a file, replacing a dependency or deploying a new revision. By the time someone approves a sensitive operation, we need the actual action and target in front of them.

The host supplies argument validation and resource resolution. Agent Guard stores the resulting action, and approval belongs to that action. The host authenticates the reviewer before calling the approval API, and the guard checks the approval again when it dispatches the stored action.

We tested this with the embedded core and a simulated deployment. After approving revision r17, we changed the caller’s original arguments to r99. The handler still received r17. In a second case, we took an approval for r18 and supplied it to a separately prepared r19 action. That returned APPROVAL_INVALID, with no handler call. The original r18 action still worked. Trying it again returned ACTION_USED, again without calling the handler. Runnable approval test · Recorded results

Then we changed the resource underneath the approved action. Our simulated service had configuration version config-v1 when we prepared the deployment. We changed it to config-v2 before execution. The handler started, compared the approved version with the current one and refused to write. A fresh preparation and approval against config-v2 succeeded.

That version comparison belongs in the trusted adapter performing the operation. Guard carries the approved version to it. For a real service, comparing the version and making the change must happen atomically, so another write cannot slip between the check and the effect. Our fixture does both synchronously in memory. A remote integration needs an equivalent service-side condition. Resource-version contract

The handler failed after making the change

The next test returned an error too, but this time the change had happened.

We used the durable executor and registered a handler that wrote one receipt to a private temporary file. Immediately after writing it, the handler threw an error. This simulated losing a usable response after the operation had completed. The caller received TOOL_FAILED. The executor recorded the outcome as UNKNOWN, and the receipt showed one completed effect.

A retry loop looking only at that error would have a reason to try again. In this case it would be repeating work that had already finished. We see the same problem with ordinary APIs when a connection breaks after a payment or deployment request has been accepted. An agent choosing its next step from the tool response has the same incomplete information.

The executor had already consumed the approval and reserved the task’s budget before dispatch. Replaying that prepared action returned ACTION_USED. We had also set the task’s concurrency limit to one. Because the first operation was unresolved, its reservation remained in place and preparing another invocation returned CONCURRENCY_EXCEEDED.

We closed the executor, reopened the same SQLite ledger with the same key and reviewed the tool registration again. The unresolved operation and its reservation were still there. Restarting had not given the task another chance to use the same approval. This was a graceful restart after the handler failed, rather than a process killed during dispatch. Runnable recovery test · Recorded results

Our test operator then checked the receipt and reconciled the record as successful. The executor released the reservation and allowed another preparation, which we cancelled without executing. The effect count stayed at one. A second attempt to settle the same record was rejected.

The operator supplies the evidence for that reconciliation. Guard cannot independently know what a remote backend did. It also does not deduplicate every new action with equivalent arguments. Our limit of one concurrent call held up new work while the outcome was unknown. After settlement, another properly authorized action could run. Backends still need idempotency keys or a reliable way to look up what happened. Durable admission and recovery

Connected tools can change too

The managed MCP integration also checks which connection and session an approved tool belongs to. A tool-list change quarantines the registered operations. Refreshing the registry requires fresh review and approval, even when the descriptors look unchanged. Reconnecting establishes a new session.

This makes a changed description or schema something the host has to deal with before work continues. It does not reveal a malicious server implementation that keeps the same descriptor. The remote server still needs its own authorization, and the host must control credentials and network access. The managed connection’s protections also differ from the older single-tool bridge, so choosing the integration matters. Managed MCP connection behavior

What we needed to explain the failed call

For the operation that wrote its receipt and then failed, we needed two records. The executor showed that it had admitted the action and still considered the outcome unknown. The receipt showed that the local backend had completed the write. An inventory of the task’s permissions could not settle that outcome.

Agent Map supplies the inventory part. Its scoped queries show visible tasks, grants, tool connections and host-declared resource bindings. They help us find a possible route and identify stale or missing observations. The snapshots exclude action and approval records, so a reviewer investigating a particular call also needs its execution history. In our test, the operator reconciled the outcome using the receipt. Agent Map queries · Inventory contents

Using the current release

The project is Apache-2.0 and remains an experimental alpha. The embedded core keeps state in memory. The durable executor uses an encrypted SQLite ledger with one owner on a local filesystem. Its state survives a restart, but it does not provide a shared authorization system for independently operating executors. Credentials, trusted adapters and the operating-system boundary remain part of the integration. Protected operations must go through the guard. An unrestricted shell or raw SDK client would give the agent another route around those checks. Published release and package scope

To run both examples, save the linked scripts in an empty directory and install the exact versions we used. The executor requires Node.js 22.13 or newer. We ran these tests on Node.js 26.12.0.

sh code example
npm init -y
npm install --ignore-scripts --save-exact @shieldaai/agent-guard@0.1.0-alpha.2 @shieldaai/agent-guard-executor@0.1.0-alpha.2
node approval-proof.mjs
node uncertain-outcome.mjs

These scripts use a simulated reviewer and local state. They check authorization and recovery behavior without a model or a real deployment. The broader project also has twelve published scenarios with paired allowed and forbidden cases, so an integration can check that useful work still runs. Scenario catalog

When the backend’s status is unknown, the operator needs the previous attempt’s identifier and a way to check it. A fresh approval can authorize more work, but it cannot establish that the first attempt did nothing. We would resolve that question before allowing another attempt at the same change.

Read part three: Security Watch when no alert arrives. · Start the series with the security problems we already know.