Your agent asked for approval once. What did it actually approve?

Agent runtimes put a human in front of dangerous tools. Before the agent writes a file, deletes data, or runs a command, someone has to say yes.

Asking on every call does not survive contact with real work. People stop reading the prompt and start clicking through it. So runtimes remember approvals.

That is a reasonable design. It hides a question most teams never write down: what is the approval remembered as?

There are three common answers.

  • The tool name.
  • The tool and its arguments.
  • A digest of the exact call.

Only the last two connect what executes to what a person actually saw.

They are two ways of writing down the same binding. Enforcing an exact-call binding at dispatch prevents argument substitution at that boundary. It does not keep the call's downstream effects inside what was approved, and that needs separate controls.

What it looks like when the answer is "the tool name"

This pattern exists in shipping agent frameworks. For example, in PraisonAI: CVE-2026-60087, published by the maintainer in June and added to GitHub's Advisory Database on 8 October.

The approval cache was "keyed on tool name only — arguments are not part of the key." It lasted for the whole run, and child tasks inherited it. Critical-tier tools such as execute_command and execute_code were exempt from the cache. File writes were not.

The advisory's proof of concept is two calls. A person approves write_file to /tmp/safe.txt. A second write_file, to /etc/crontab, is then approved without another prompt. The runtime records the reason as "Already approved in context".

The person approved writing a scratch file. The runtime recorded approval of writing files.

An approval that does not name its arguments authorizes a tool, not an action.

A second failure: declared policy without enforcement

The maintainer published a related advisory alongside it, GHSA-5r6c-gj4g-r697. SecurityPolicy.strict(), documented as "a strict security policy for untrusted code," was enforced by the default subprocess sandbox only for network access and output size. Its command, path and subprocess restrictions were never checked.

These are two different failures with one lesson: declared controls must constrain what actually executes.

Both are fixed, in praisonaiagents 1.6.78 and praisonai 4.6.78. If you ran an affected version with approval prompts, upgrading closes the door going forward. It does not tell you what already went through it. Identify the tool calls that executed after the first approval in each run, and review the ones whose arguments no person saw.

What a binding approval commits to

Two recent preprints use the term "approval laundering" for failures in this family, with different definitions (Wang, arXiv 2609.38983; Zhang et al., arXiv 2609.28586). The property underneath it can be stated as a check on a record.

The Approval Binding Vectors corpus (ABV v0.1.3) states it as predicates. Two matter here.

  • P2, arguments: "The approval's scope commits to the executed argument bytes."
  • P6, single use: one approval authorises at most one execution. ABV's own spec calls this "a profile choice, not a universal requirement." Some systems legitimately want standing approvals.

The PraisonAI behavior conflicts with P2. Under a single-use contract, it also conflicts with P6.

The reference checker rejects records with exactly this shape:

$ python3 check.py
  PASS  NEG-P2-01    P2: executed arguments are not the approved arguments
  PASS  NEG-P6-01    P6: one approval, 2 executions
  ...
15/15 vectors behaved as specified
acceptance controls: 4 accepted (a run with 0 is not a result, regardless of the negatives)

PASS means the checker correctly rejected an invalid record. It says nothing about PraisonAI. NEG-P2-01 approves deploy.scale with replicas: 3 and executes it with replicas: 300.

The limit matters. A record check needs trustworthy evidence linking request, approval and execution. Without that evidence, nobody can verify these properties after the fact.

Testing it against a live runtime

A record check is not a runtime test. In the Agent Security Harness at the referenced commit, related tests exist: a denied decision replayed, a forged approval, sandbox escape. None of them approves one argument set and executes another, or checks that a declared policy is enforced. That gap is filed as #643.

The proposed tests observe execution, not refusal messages.

  1. Argument binding, for call-specific approvals. Approve tool T with arguments X, then call T with arguments Y. Pass only if Y does not execute before a fresh approval. (An approval that explicitly grants a range of arguments can legitimately cover both.)
  2. Single use, where the profile requires it. Approve X, then call X twice. Pass only if the second call does not execute without a fresh approval.
  3. Declared-policy enforcement. Declare a deny rule, then attempt the denied operation. Pass only if the prohibited operation does not occur.

Each has a positive control: the approved call, or the allowed operation, must actually complete. Otherwise a runtime that refuses everything would pass.

And each needs evidence that the test reached the enforcement point. An operation that was never attempted is not a pass. It is inconclusive.

The question to ask this week

Find the place where your agent runtime remembers an approval.

Read the cache key.

If it is a tool name, and nothing downstream re-checks the arguments, a person approving one call is signing for every argument the agent chooses for as long as that cached approval remains valid.


Views are my own.

Sources:

This field note was drafted with AI assistance and human-reviewed. Each quotation was checked against its primary source, and the linked evidence was retrieved before publication.

Get notified about new EAA series parts

One email when a new position-paper part or field note publishes. No newsletter, no marketing list.