Confirmations
Pre-Authorized Action Execution — what the user sees IS what fires
When a destructive (or chain) action would run, the web-kernel intercepts it, shows the user a confirmation card, and only executes after the user clicks "yes". This page explains the flow, the federal guarantees, and how your extension participates.
The user-facing flow
You: clear my done tasks
Webbee: "This will delete 3 completed tasks. Confirm?" — [✅ Yes] [✗ Cancel]
You: (clicks ✅)
Webbee: ✓ Cleared 3 completed task(s).
What happened end to end:
Classifier picks tasks-mini.clear_done_tasks from the user's intent.
Web-kernel sees action_type="destructive" on your tool, intercepts the call, stores the args, emits a ConfirmationCard to chat.
The Imperal Panel renders the card with [Yes] and [Cancel] buttons.
The user clicks. Auth Gateway receives POST /v1/confirmations/<conf_id>/accept with the user's JWT.
Auth Gateway verifies the user matches the original request, looks up the stored intercepted_calls, dispatches them via the typed-iterate path. Your handler runs with byte-identical args.
Audit chokepoint records the action with confirmation=user_accepted, retention federal_7y.
What you do
Just declare action_type="destructive":
@chat.function(
description="Permanently delete a folder and all its contents.",
action_type="destructive",
id_projection="folder_id",
)
async def delete_folder(ctx, params: DeleteFolderParams):
await ctx.http.delete(f"/folders/{params.folder_id}")
return {"text": "Folder deleted."}That's it. The web-kernel handles the rest.
What the user sees
The confirmation card includes:
| Element | Source |
|---|---|
| Title ("Delete folder?") | i18n template by action_type |
| Description | Your tool's description (so write it well) |
| Function call entries | Each intercepted call rendered: tool name + key params |
| Action buttons | [Yes, do it] / [Cancel] |
Example for a chain (multiple destructive calls in one user turn):
Confirm 3 actions:
1. delete_note(note_id="note_abc")
2. delete_note(note_id="note_def")
3. archive_thread(thread_id="thr_123")
[✅ Confirm all] [✗ Cancel]Federal I-CONFIRMATION-EXECUTES-WHAT-USER-SAW means: all three run with those exact args, in that order, on accept. No re-planning, no LLM rerun.
The federal core
I-CONFIRMATION-EXECUTES-WHAT-USER-SAW
What the card shows IS what runs. Byte-identical args via stored intercepted_calls.
I-CONFIRMATIONS-NO-BODY
Confirmations endpoint accepts NO body params. Args come from web-kernel storage, never the request.
I-AA-FU-7-CONFIRMATIONS-STRICT-DEP
Header X-Acting-User REQUIRED. Body/query fallback rejected with 400.
I-AA-FU-8 — confirmation TTL clamp
Confirmations expire alongside chat history TTL. No zombie acceptances after sessions end.
I-CHAIN-PREFLIGHT-RESPECTS-USER-TOGGLE
Pre-flight cards consult kctx.confirmation_enabled + confirmation_actions on BOTH the single-action and multi-step chain paths — users with 2-step OFF see uniform behaviour across plan shapes.
Multi-step chains and confirmations
When a user says "check my unread mail and delete the spam":
- Classifier emits a 2-step chain:
[mail.list_unread → mail.delete_spam] - Web-kernel runs step 1 (read-only — fires immediately)
- Step 2 is destructive — web-kernel pauses, intercepts args, shows card with the actual count to be deleted (precondition_iterate kind)
- User accepts → step 2 runs typed-iterate
The card shows real numbers ("Delete 7 spam threads?"), not placeholders ("Delete some threads?"). Federal Phase 3 of Pre-Authorized Action Execution covers this.
What you should NOT do
Per-action override (rare)
You can force confirmation on a write tool with confirm: True in panel actions:
from imperal_sdk import ui
@ext.panel("items", slot="left", title="Items")
async def items_sidebar(ctx, **kwargs):
items = await ctx.store.query("items", limit=50)
return ui.List(items=[
ui.ListItem(
id=str(item["id"]),
title=item["name"],
actions=[{
"label": "Move to trash",
"on_click": ui.Call("archive_item", item_id=item["id"], confirm=True),
}],
)
for item in items.data
])Original (legacy @panels.sidebar + dict-shape) for reference:
# LEGACY — pre-v4 SDK. Does NOT work today. Kept for context.
@panels.sidebar(...)
async def items_sidebar(ctx):
return {
"type": "list",
"items": [
{"label": item.name, "actions": [
{"label": "Move to trash", "tool": "items.archive",
"args": {"item_id": item.id}, "confirm": True},
]}
],
}Useful for write actions that aren't strictly destructive but the UI wants to double-check.