ADR-210: A caller can ask which tools run without approval 

Status

Accepted

Date

2026-09-26

Authors

Netresearch DTT GmbH

Context 

ToolLoopServiceInterface::runLoop() suspends a run as soon as the model calls a tool that needs a human approval, and throws ToolApprovalRequiredException with the state to resume from (ADR-084). A caller that has no approval step, such as an editor dialog that runs one task and shows the answer, cannot resume, so for it the exception is a failed run.

Such a caller wants to offer the model only the tools that never suspend. The rule that decides this, ToolApprovalRule::requiresApproval() (ADR-134), is @internal, and so is the registry it needs the tool from. ToolCallPolicyInterface (@api) answers which tools may be offered, but its ToolPolicyDecision says nothing about approval. A consumer is therefore left to re-derive the rule from internal classes, or to offer every tool and fail when one suspends.

Decision 

A new @api interface, UnattendedToolFilterInterface, with one method: unattended(list<string> $toolNames): list<string> returns the names, in their order, of the tools that run without an approval.

  • It asks ToolApprovalRule::requiresApproval() about the tool the registry holds under each name. The rule stays the only place that decides, so a local tool that gains a write effect, or any tool that gains the approval marker, leaves the unattended set without a change here.
  • It inherits the rule's exemption for remote tools. A remote tool is judged on the operator's declaration, not on its effect (ADR-134), so an imported tool without a declaration stays in the unattended set whatever it does — exactly as it runs without an approval in an agent run. An operator who wants such a tool kept out of a caller without an approval step declares it approval-bound on its server record.
  • A tool that asks for typed input (RequiresInputInterface) is left out as well. It needs no approval, but the loop suspends for its input (WAITING_FOR_INPUT), and a caller without an approval step has no input step either. "Unattended" means no human pause of either kind.
  • A name the registry does not know is left out. An unknown tool cannot be shown to be free of approval, and leaving it in would offer the model a call that suspends.
  • It filters; it does not decide what may be offered. A caller first asks ToolCallPolicyInterface::filterOfferable() and then narrows that list, so the five policy gates of ADR-094 still apply.

A separate interface, not a method on ToolCallPolicyInterface. Adding a method to an @api interface breaks every class that implements it. The two questions are also different: the policy answers whether a tool may take part in a run at all, this filter answers whether it can take part without a human.

Consequences 

● A caller without an approval step runs the tool loop on ToolCallPolicyInterface::filterOfferable() narrowed by UnattendedToolFilterInterface::unattended() and meets neither an approval nor an input suspension from a registered tool.

● The approval rule has one reader more and still one definition.

◐ An operator setting that makes a read tool approval-bound, such as the web page reader's (ADR-202), removes that tool from the unattended set too, so a caller without an approval step offers it no longer. That is what the setting asks for.

✕ The answer is per tool, not per call. A tool whose approval depends on its arguments would need a finer contract; no tool in this extension has one.