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 call | Accepted | Refused (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 executed | a 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 theapprovalIdof the call'stool_approval_requestchunk (${runId}::${toolCallId}), which the AI SDK stream carries astool-approval-request { approvalId, toolCallId }and a stockuseChatsends back throughaddToolApprovalResponse({ id }). A stockuseChatapp needs no change. Code that built anapproval-responsewith 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, sendapproved: false. An error-only answer is accepted and cancels the approval (the tool does not run). - Temporal and DBOS now record the
approvalIdon 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 withTool '<name>' is not registered, never reported as not approved.
No data migration: the entry field is the existing optional PendingClientToolCall.approvalId.