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

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. 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:

// 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) 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:

// 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 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:

// 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 manages the confirmation lifecycle. When a tool requests confirmation, addConfirmation() emits the event to the renderer:

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

The conversationBridge.ts handles the return path. When the renderer sends the user's decision, the provider retrieves the task and calls task.confirm():

// 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:

// 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:

// 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);
        }
      },
    };
  }
}
// Worker callback storage (src/worker/gemini.ts)
pipe.once(tool.callId, (confirmKey: string) => {
  onConfirm(confirmKey);
});
// 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,
  });
};
// Bridge forwards answer back to task
task.confirm(msg_id, callId, selectedOutcome);
// 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, returning ToolCallConfirmationDetails and an onConfirm callback.
  • The worker in 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 coordinates the lifecycle, while conversationBridge.ts routes user decisions back to the task.
  • The renderer displays prompts via 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. 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 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.

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 calls ipcBridge.conversation.confirmation.confirm.invoke() with the selected ToolConfirmationOutcome, conversation_id, msg_id, and callId. The 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →