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()insrc/agent/gemini/cli/tools/tools.ts, returningToolCallConfirmationDetailsand anonConfirmcallback. - The worker in
src/worker/gemini.tsserializes the request by stripping callbacks and registeringpipe.once()listeners keyed bycallId. BaseAgentManagerinsrc/process/task/BaseAgentManager.tscoordinates the lifecycle, whileconversationBridge.tsroutes user decisions back to the task.- The renderer displays prompts via
MessageToolGroup.tsxinsrc/renderer/messages/, supporting outcomes like ProceedOnce, ProceedAlways, and Cancel. - Gemini agents cache approvals in
GeminiApprovalStore, enablingconversation.approval.checkto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →