Skip to content

A submit must answer the kind of call that is pending (CP-231) ​

This guide covers the release in which every submitToolResult (the executor API, agent-server's /submit-tool-result, the CF Durable Object route and handleChatStream's path 3) is bound to the call it answers. It is a security fix: before it, a client-tool-result posted for a paused approval-gated server tool was stored and drained as that tool's successful output although the tool never ran, and the model was told it succeeded (runtime-js and the CF Durable Object, Temporal, DBOS; CF Workflows refused it only while the tool still existed).

The rule ​

The pending callAcceptedRefused (HTTP 400, code: 'validation_error', nothing stored)
an approval (its entry carries an approvalId){ kind: 'approval-response', approvalId, approved, reason? } with that approvalId; or an error-only { kind: 'client-tool-result', error } (no result), drained as not executeda client-tool-result carrying a result; an approval-response with another approvalId
a client-executed call{ kind: 'client-tool-result', result } or { …, error }an approval-response

The check runs on every submit path, a direct submit to a local sub-agent's session on runtime-js / the CF DO included. An error-only answer to an approval (the chat handler's abandonment of a pending call, client_tool_abandoned, is one) is accepted because it can never read as the tool's success: the drain pairs the call as not executed and the tool does not run. submitToolResult throws the refusal as a HelixError (retryable: false), which a typed HTTP route answers 400 (httpStatusForHelixError). Each runtime's drain also refuses a tool result that reached an approval's entry some other way: the call is paired with a framework_internal_error result and the tool never runs.

What to change ​

  • Send the approval's own approvalId. It is the approvalId of the call's tool_approval_request chunk (${runId}::${toolCallId}), which the AI SDK stream carries as tool-approval-request { approvalId, toolCallId } and a stock useChat sends back through addToolApprovalResponse({ id }). A stock useChat app needs no change. Code that built an approval-response with a made-up or stale id (tests, scripts, custom clients) is now refused.
  • Answer an approval with an approval-response, never a tool result. To reject, send approved: false. An error-only answer is accepted and cancels the approval (the tool does not run).
  • Temporal and DBOS now record the approvalId on an approval's pending entry (as runtime-js, the CF DO and CF Workflows already did), and Temporal's drain classifies an approval by it (CP-154): an approved call whose tool a deploy removed is paired with Tool '<name>' is not registered, never reported as not approved.

No data migration: the entry field is the existing optional PendingClientToolCall.approvalId.

Released under the MIT License.