# AionUi Tool Confirmation Flow: Secure AI Agent Execution for File Operations

> Learn about AionUi's secure three-stage confirmation flow protecting AI agent file operations. Get user approval before dangerous command execution.

- Repository: [OfficeAI/AionUi](https://github.com/iofficeai/aionui)
- Tags: internals
- Published: 2026-02-16

---

**AionUi implements a three-stage IPC-based confirmation flow where tools request user approval through a renderer UI before executing dangerous file operations or system commands.**

AionUi, developed by iOfficeAI, is an open-source AI agent framework that requires explicit user consent before performing destructive actions. The **AionUi tool confirmation flow** ensures that file modifications, command executions, and other dangerous operations are blocked until the user reviews the request through a secure, asynchronous IPC pipeline.

## The Three-Stage Confirmation Architecture

The confirmation system operates through three distinct stages that separate tool logic from UI presentation.

### Stage 1: Tool Request Generation

When a tool requires confirmation, it implements the `shouldConfirmExecute()` method in [`src/agent/gemini/cli/tools/tools.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/agent/gemini/cli/tools/tools.ts). This method returns a `ToolCallConfirmationDetails` object containing:

- **type**: Classification (`'edit'`, `'exec'`, `'mcp'`, or `'info'`)
- **title**: Human-readable description of the action
- **onConfirm**: Async callback executed after user decision

The `ToolConfirmationOutcome` enum defines possible user responses:

```typescript
// src/agent/gemini/cli/tools/tools.ts
export enum ToolConfirmationOutcome {
  ProceedOnce = 'proceed_once',
  ProceedAlways = 'proceed_always',
  ProceedAlwaysServer = 'proceed_always_server',
  ProceedAlwaysTool = 'proceed_always_tool',
  ModifyWithEditor = 'modify_with_editor',
  Cancel = 'cancel',
}

```

### Stage 2: Worker IPC Bridge Handling

The worker process (e.g., [`src/worker/gemini.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/worker/gemini.ts)) strips the `onConfirm` callback from the payload to ensure it can be serialized over IPC. It stores the callback locally and registers a one-time listener using `pipe.once()` keyed by the tool's `callId`:

```typescript
// src/worker/gemini.ts
if (confirmationDetails) {
  const { onConfirm, ...details } = confirmationDetails;
  // Store the callback and wait for UI to send back a key
  pipe.once(tool.callId, (confirmKey: string) => {
    onConfirm(confirmKey);
  });
  return { ...other, confirmationDetails: details };
}

```

### Stage 3: Renderer UI and User Decision

The renderer's `MessageToolGroup` component in [`src/renderer/messages/MessageToolGroup.tsx`](https://github.com/iOfficeAI/AionUi/blob/main/src/renderer/messages/MessageToolGroup.tsx) receives the confirmation payload via the `ipcBridge.conversation.confirmation.add` event. It renders action buttons (Proceed Once, Proceed Always, Cancel) and invokes `ipcBridge.conversation.confirmation.confirm` when the user selects an option:

```tsx
// src/renderer/messages/MessageToolGroup.tsx
const handleConfirm = async (outcome: ToolConfirmationOutcome) => {
  await ipcBridge.conversation.confirmation.confirm.invoke({
    conversation_id,
    msg_id,
    data: outcome,
    callId: confirmation.id,
  });
};

```

## Task Management and Bridge Routing

The `BaseAgentManager` class in [`src/process/task/BaseAgentManager.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/process/task/BaseAgentManager.ts) manages the confirmation lifecycle. When a tool requests confirmation, `addConfirmation()` emits the event to the renderer:

```typescript
// src/process/task/BaseAgentManager.ts
protected addConfirmation(data: IConfirmation<ConfirmationOption>) {
  ipcBridge.conversation.confirmation.add.emit({ 
    ...data, 
    conversation_id: this.conversation_id 
  });
}

```

The [`conversationBridge.ts`](https://github.com/iOfficeAI/AionUi/blob/main/conversationBridge.ts) handles the return path. When the renderer sends the user's decision, the provider retrieves the task and calls `task.confirm()`:

```typescript
// src/process/bridge/conversationBridge.ts
ipcBridge.conversation.confirmation.confirm.provider(async ({ conversation_id, msg_id, data, callId }) => {
  const task = WorkerManage.getTaskById(conversation_id);
  if (!task) return { success: false, msg: 'conversation not found' };
  task.confirm(msg_id, callId, data);
  return { success: true };
});

```

## Always-Allow Memory for Gemini Agents

For Gemini agents, AionUi implements a persistent approval cache via `GeminiApprovalStore`. When a user selects **Proceed Always**, the decision is stored per-conversation. Subsequent identical actions bypass the UI after checking `conversation.approval.check`:

```typescript
// src/process/bridge/conversationBridge.ts
ipcBridge.conversation.approval.check.provider(async ({ conversation_id, action, commandType }) => {
  const task = WorkerManage.getTaskById(conversation_id) as GeminiAgentManager | undefined;
  if (!task || task.type !== 'gemini' || !task.approvalStore) return false;
  const keys = GeminiApprovalStore.createKeysFromConfirmation(action, commandType);
  return task.approvalStore.allApproved(keys);
});

```

## End-to-End Implementation Example

The following example demonstrates a complete flow for a file deletion tool:

```typescript
// Tool definition (src/agent/gemini/cli/tools/tools.ts)
class DeleteFileTool extends BaseDeclarativeTool<{ path: string }, ToolResult> {
  async shouldConfirmExecute(_signal: AbortSignal) {
    return {
      type: 'exec',
      title: `Delete file "${this.params.path}"?`,
      command: `rm "${this.params.path}"`,
      onConfirm: async (outcome) => {
        if (outcome === ToolConfirmationOutcome.ProceedOnce ||
            outcome === ToolConfirmationOutcome.ProceedAlways) {
          await fs.unlink(this.params.path);
        }
      },
    };
  }
}

```

```typescript
// Worker callback storage (src/worker/gemini.ts)
pipe.once(tool.callId, (confirmKey: string) => {
  onConfirm(confirmKey);
});

```

```tsx
// Renderer UI handling (src/renderer/messages/MessageToolGroup.tsx)
const handleConfirm = async (outcome: ToolConfirmationOutcome) => {
  await ipcBridge.conversation.confirmation.confirm.invoke({
    conversation_id,
    msg_id,
    data: outcome,
    callId: confirmation.id,
  });
};

```

```typescript
// Bridge forwards answer back to task
task.confirm(msg_id, callId, selectedOutcome);

```

```typescript
// Task invokes stored callback
// The `pipe.once` fires, invoking the tool's onConfirm
// → File is actually deleted (or not) according to the outcome

```

## Summary

- **AionUi's tool confirmation flow** uses a three-stage pipeline separating tool logic, IPC transport, and UI presentation.
- Tools declare confirmation requirements via `shouldConfirmExecute()` in [`src/agent/gemini/cli/tools/tools.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/agent/gemini/cli/tools/tools.ts), returning `ToolCallConfirmationDetails` and an `onConfirm` callback.
- The worker in [`src/worker/gemini.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/worker/gemini.ts) serializes the request by stripping callbacks and registering `pipe.once()` listeners keyed by `callId`.
- `BaseAgentManager` in [`src/process/task/BaseAgentManager.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/process/task/BaseAgentManager.ts) coordinates the lifecycle, while [`conversationBridge.ts`](https://github.com/iOfficeAI/AionUi/blob/main/conversationBridge.ts) routes user decisions back to the task.
- The renderer displays prompts via [`MessageToolGroup.tsx`](https://github.com/iOfficeAI/AionUi/blob/main/MessageToolGroup.tsx) in `src/renderer/messages/`, supporting outcomes like **ProceedOnce**, **ProceedAlways**, and **Cancel**.
- Gemini agents cache approvals in `GeminiApprovalStore`, enabling `conversation.approval.check` to skip redundant prompts for repeated actions.

## Frequently Asked Questions

### How does AionUi determine which AI agent actions require confirmation?

Tools implement the `shouldConfirmExecute()` method to signal when user approval is needed. This method returns a `ToolCallConfirmationDetails` object for actions classified as `'edit'`, `'exec'`, `'mcp'`, or `'info'` in [`src/agent/gemini/cli/tools/tools.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/agent/gemini/cli/tools/tools.ts). If the method returns `null` or `undefined`, the tool executes immediately without prompting the user.

### What happens to the tool execution callback during IPC serialization?

The worker process in [`src/worker/gemini.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/worker/gemini.ts) strips the non-serializable `onConfirm` callback from the confirmation payload before sending it across the IPC boundary. It stores the callback locally in memory and registers a one-time listener using `pipe.once(tool.callId, ...)`. When the renderer returns the user's decision, the worker retrieves the callback and invokes it with the selected `ToolConfirmationOutcome`.

### Can users permanently approve specific tool types in AionUi?

Yes, for Gemini agents, AionUi provides an "always-allow" memory system via `GeminiApprovalStore`. When a user selects **ProceedAlways**, **ProceedAlwaysServer**, or **ProceedAlwaysTool**, the decision is cached per-conversation. Subsequent identical actions bypass the UI prompt after `conversation.approval.check` verifies the stored approval keys in [`src/process/bridge/conversationBridge.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/process/bridge/conversationBridge.ts).

### How does the renderer communicate the user's confirmation choice back to the tool?

The renderer's `MessageToolGroup` component in [`src/renderer/messages/MessageToolGroup.tsx`](https://github.com/iOfficeAI/AionUi/blob/main/src/renderer/messages/MessageToolGroup.tsx) calls `ipcBridge.conversation.confirmation.confirm.invoke()` with the selected `ToolConfirmationOutcome`, `conversation_id`, `msg_id`, and `callId`. The [`conversationBridge.ts`](https://github.com/iOfficeAI/AionUi/blob/main/conversationBridge.ts) provider receives this invocation, retrieves the corresponding task via `WorkerManage.getTaskById()`, and calls `task.confirm()`. This triggers the worker's `pipe.once` listener, which finally executes the tool's stored `onConfirm` callback.