DOCS
Tool confirmation
Sensitive tools pause the turn until the chat user taps Confirm or Deny. Opt-in only — nothing runs with confirmation unless the host or client tool declares it.
Tool confirmation
Opt-in gate before destructive or sensitive host and frontend tools run. The chat user approves or denies in the UI; the runtime executes only after approve, using arguments from the stored turn checkpoint.
What it does
Some tools should not run the moment the model asks for them — for example deleting a row, charging a card, or
sending mail. With confirmation enabled, the agent loop stops after emitting
tool.call
and waits for an explicit chat decision. The host MCP handler or frontend handler runs only after
approve.
On
deny
the tool is not called; the model receives a structured failure instead.
- Opt-in.
Tools without the flag stay automatic. Existing
create_post/update_postbehavior is unchanged. - Not the same as mode.
mode: deleteon a host tool is a policy hint ( policies ). It does not turn on chat confirmation by itself. - Not renderHint.
renderHint: confirm_dialogontool.resultis unused for this flow. The decision happens before execution, ontool.call. - Client cannot disable host confirmation.
The flag comes from MCP
_metaor from the tool catalog the server already listed — not from a field the browser adds to the message body.
Declaring confirmation
Two channels share one runtime rule: publish
confirmation: "required"
only when you want the chat gate. Omit the key or use
auto
for automatic execution.
Host MCP tools
Host SDKs map an optional
confirmation()
/
confirmation
field on the tool class or object into MCP
tools/list
metadata. Custom MCP servers (not using our SDK) should set the same shape on
_meta.
{
"name": "delete_post",
"description": "…",
"_meta": {
"domain": "posts",
"mode": "delete",
"confirmation": "required"
}
}{
"name": "delete_post",
"description": "…",
"_meta": {
"domain": "posts",
"mode": "delete",
"confirmation": "required"
}
} export const deletePostTool = {
name: 'delete_post',
description: 'Delete a post. The chat user must confirm before it runs.',
mode: MODE_DELETE,
domain: 'posts',
confirmation: 'required',
schema() {
return { post_id: { type: 'integer', required: true } }
},
handle(args) { … },
}export const deletePostTool = {
name: 'delete_post',
description: 'Delete a post. The chat user must confirm before it runs.',
mode: MODE_DELETE,
domain: 'posts',
confirmation: 'required',
schema() {
return { post_id: { type: 'integer', required: true } }
},
handle(args) { … },
} class DeletePostTool implements HostTool
{
public function confirmation(): string
{
return 'required';
}
public function mode(): string
{
return self::MODE_DELETE;
}
// name, schema, handle …
}class DeletePostTool implements HostTool
{
public function confirmation(): string
{
return 'required';
}
public function mode(): string
{
return self::MODE_DELETE;
}
// name, schema, handle …
}
Other host packages follow the same contract: Java
HostTool.confirmation(),
.NET
IHostTool.Confirmation,
Python
confirmation
on the tool class,
Ruby
confirmation,
Go
ConfirmingTool.
See each
host SDK guide.
Frontend (client) tools
Register on
SvedaToolRegistry
with
confirmation: 'required'.
The registry sends it on every stream body as
clientTools
(only when required — auto tools omit the field). Details:
MCP and tools.
tools.register({
name: 'archive_selection',
description: 'Archive the current UI selection',
confirmation: 'required',
parameters: { type: 'object', properties: {} },
handler: async (input, { chatId, toolCallId }) => ({ … }),
})tools.register({
name: 'archive_selection',
description: 'Archive the current UI selection',
confirmation: 'required',
parameters: { type: 'object', properties: {} },
handler: async (input, { chatId, toolCallId }) => ({ … }),
}) Stream protocol
When the model selects a confirming tool, the runtime emits
tool.call
with optional
confirmation: "required"
alongside
toolCallId,
toolName,
input, and
target
(
backend
or
frontend
). It does not call the host or the browser handler yet.
data: {"type":"tool.call","toolCallId":"call_del_1","toolName":"delete_post","target":"backend","input":{"post_id":1},"confirmation":"required"}data: {"type":"tool.call","toolCallId":"call_del_1","toolName":"delete_post","target":"backend","input":{"post_id":1},"confirmation":"required"}
The turn ends with
message.end
and
finishReason
consistent with other tool pauses. Other tools in the same model step still run if they do not require confirmation;
confirming calls wait until every pending decision has a
tool.result.
Resuming with toolDecisions
The client sends a follow-up
POST /sveda/stream
with an empty
prompt,
the current
messages
(including the assistant message that owns the tool call), and
toolDecisions.
Each entry is
{ toolCallId, decision: "approve" | "deny" }
— no arguments. The server loads the original
input
from the stored chat checkpoint so the client cannot swap ids or parameters at decision time.
POST /sveda/stream
Content-Type: application/json
x-sveda-embed-token: sveda_embed_…
{
"chatId": "chat_1",
"prompt": "",
"messages": [ … ],
"toolDecisions": [
{ "toolCallId": "call_del_1", "decision": "approve" }
]
}POST /sveda/stream
Content-Type: application/json
x-sveda-embed-token: sveda_embed_…
{
"chatId": "chat_1",
"prompt": "",
"messages": [ … ],
"toolDecisions": [
{ "toolCallId": "call_del_1", "decision": "approve" }
]
} - approve + backend:
runtime calls host MCP
tools/callonce with stored arguments; duplicate approve is a no-op. - approve + frontend:
@sveda-ai/coreruns the registered handler locally, then poststool.resulton the stream (same as other frontend tools). - deny:
host is not called; result includes
denied: trueand a fixed error string (see below).
{
"success": false,
"denied": true,
"error": "The user denied this action and it was not executed."
}{
"success": false,
"denied": true,
"error": "The user denied this action and it was not executed."
} Full event list: stream protocol.
Server rules
-
Pending confirmation and a new user message without decisions → HTTP
409Tool confirmation is required before the conversation can continue. -
Decisions but no stored chat history →
409 Chat history is unavailable. - Decisions when nothing is pending → ignored; the turn continues normally.
-
Unresolved confirming calls after decisions → emit
tool.resultonly; do not call the model until every confirming call has a result. -
spawn_tasksrefuses confirmation-required tools with an error (subagents cannot show the chat UI). -
Display/history merges
tool.resultinto the existing assistant message bytoolCallIdand keepsconfirmation: "required"on parts so buttons survive refresh.
Chat UI
@sveda-ai/vue
(embed and
<sveda-chat>
) renders a compact card under the tool row: prompt text, JSON arguments, Confirm and Deny buttons. The composer
is disabled while any tool call in the current chat still has
confirmation: "required"
and no result. iOS uses the same protocol fields and sends
toolDecisions
from the native chat surface.
import { useSvedaChatPage } from '@sveda-ai/vue'
const chat = useSvedaChatPage({ … })
// chat.confirmationPending — composer locked while a tool waits
// @confirm-tool / @deny-tool → session.resolveToolConfirmation(toolCallId, 'approve' | 'deny')import { useSvedaChatPage } from '@sveda-ai/vue'
const chat = useSvedaChatPage({ … })
// chat.confirmationPending — composer locked while a tool waits
// @confirm-tool / @deny-tool → session.resolveToolConfirmation(toolCallId, 'approve' | 'deny')
Custom UIs should call
session.resolveToolConfirmation(toolCallId, 'approve' | 'deny')
on
SvedaChatSession
(
JS client
). Do not auto-run frontend handlers for confirming tools while
autoSubmitFrontendToolResults
is on — the core client skips them until approve.
Policies and visibility
Confirmation only matters if the model can see the tool. Embed policies filter host MCP tools on every turn:
mcp.allow
must include the tool name (or a matching glob), and
max_mode
must allow the tool mode. Delete tools need
max_mode: "delete"
— a policy capped at
write
hides
delete_post
from the model even when the host registers it.
{
"agent": {
"web": true,
"code": false,
"mcp": {
"allow": ["search_posts", "create_post", "update_post", "delete_post"],
"domains": ["posts"],
"max_mode": "delete"
},
"client": { "allow": [] }
}
}{
"agent": {
"web": true,
"code": false,
"mcp": {
"allow": ["search_posts", "create_post", "update_post", "delete_post"],
"domains": ["posts"],
"max_mode": "delete"
},
"client": { "allow": [] }
}
}
The host still chooses which tools to expose per user via
resolveToolsUsing
; that list cannot widen past the token policy. Row-level rules (only delete your own rows) stay in the host
handle
method — confirmation is an extra UX gate, not authorization.
Try it in the playground
Laravel (and other) SDK playgrounds register
delete_post
with
confirmation: required
for agent users. Set the runtime
agent
policy to allow
delete_post
and
max_mode: delete
at
sveda.yaml, open the JS chat, and ask to delete a post by id.