Imperal Docs
Guides

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:

ElementSource
Title ("Delete folder?")i18n template by action_type
DescriptionYour tool's description (so write it well)
Function call entriesEach 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":

  1. Classifier emits a 2-step chain: [mail.list_unread → mail.delete_spam]
  2. Web-kernel runs step 1 (read-only — fires immediately)
  3. Step 2 is destructive — web-kernel pauses, intercepts args, shows card with the actual count to be deleted (precondition_iterate kind)
  4. 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.

Where to next

On this page