Trace Tool Approval Requests in Kimi CLI: From Execution to User Prompt
Tool approval requests in Kimi CLI flow from the tool layer through an approval runtime, across the wire protocol to the UI, and back to the tool via async resolution.
The Kimi CLI codebase implements a permission gate for any tool that can modify the system or external resources. Understanding how it handles tool approval requests requires tracing the path from src/kimi_cli/soul/approval.py through the runtime and wire layers to the React frontend and back again.
Stage 1: Tool Execution Creates the Request
When a tool requires user consent, it invokes Approval.request(...) in src/kimi_cli/soul/approval.py. This method first checks the current tool call context using get_current_tool_call_or_none to associate the request with the correct call ID.
The method then builds an ApprovalRequestRecord defined in src/kimi_cli/approval_runtime/models.py and registers it with an ApprovalRuntime instance. As implemented in src/kimi_cli/approval_runtime/runtime.py, the ApprovalRuntime.create_request method persists the pending request and publishes a request_created event. Simultaneously, the request is serialized into an ApprovalRequest message and dispatched to the wire layer.
# ---- Inside a tool implementation (e.g., a file‑write tool) ----
async def run(self, path: str, content: str):
# Request user approval before writing the file
approval = await self.approval.request(
sender="file_tool",
action="write_file",
description=f"Write {len(content)} bytes to {path}",
display=[DisplayBlock(type="diff", data=diff_of_content)],
)
if not approval:
raise approval.rejection_error() # raises ToolRejectedError
# Approved – perform the write
with open(path, "w") as f:
f.write(content)
If the session operates in AFK or YOLO mode, Approval.is_auto_approve() short-circuits the flow and immediately returns an approved ApprovalResult without prompting the user.
Stage 2: Wire Protocol Delivers the Request to the UI
The wire hub receives the ApprovalRequest and forwards it to all UI subscribers. In the React frontend, the ApprovalDialog component in web/src/features/chat/components/approval-dialog.tsx monitors incoming chat messages. It locates the message whose variant === "tool" and whose approval field indicates an approval‑requested state.
From that message, the component extracts the request ID, description, action, and optional display blocks. It then renders a modal with Approve, Approve for session, Decline, and Decline with feedback buttons.
// ---- React component handling the pending request ----
const handleResponse = useCallback(
async (decision: ApprovalResponseDecision, reason?: string) => {
if (pendingApproval && onApprovalResponse) {
await onApprovalResponse(pendingApproval.approval.id, decision, reason);
}
},
[pendingApproval, onApprovalResponse],
);
Stage 3: User Response Resolves the Pending Request
When the user clicks a button or triggers a keyboard shortcut, ApprovalDialog invokes the onApprovalResponse callback with the request ID and a decision. The wire layer routes this back to ApprovalRuntime.resolve(request_id, decision, ...) in src/kimi_cli/approval_runtime/runtime.py.
The runtime marks the request as resolved, records any feedback, and publishes a request_resolved event. It then wakes the awaiting coroutine in Approval.request() by setting the result on a waiter future. The original tool call receives an ApprovalResult object. If approved, execution continues; otherwise, the runtime raises a ToolRejectedError.
# ---- Runtime side that resolves the request ----
def resolve(self, request_id: str, decision: ApprovalResponseKind, feedback: str = "", approved_via_session_cache: bool = False):
request = self._requests[request_id]
request.status = "resolved"
request.response = decision
request.feedback = feedback
request.approved_via_session_cache = approved_via_session_cache
self._publish_event(ApprovalRuntimeEvent(kind="request_resolved", request=request))
# Wake the awaiting coroutine in Approval.request()
waiter = self._waiters.pop(request_id, None)
if waiter and not waiter.done():
waiter.set_result((decision, feedback))
If the user selects Approve for session, the action name is added to self._state.auto_approve_actions. Future tool approval requests with the same action bypass the UI and are auto‑approved. Every outcome branch emits a permission_approval_result telemetry event via _track_permission_result.
Summary
- Entry point: Tools call
Approval.request()insrc/kimi_cli/soul/approval.pyto initiate tool approval requests. - State management:
ApprovalRuntimeinsrc/kimi_cli/approval_runtime/runtime.pycreates, stores, and resolvesApprovalRequestRecordinstances. - Data models:
src/kimi_cli/approval_runtime/models.pydefines the record types used during the flow. - UI rendering: The
ApprovalDialogcomponent inweb/src/features/chat/components/approval-dialog.tsxdisplays pending requests and captures user decisions. - Async resolution: The runtime wakes the blocked tool coroutine with an
ApprovalResultor raisesToolRejectedErrorwhen the user declines. - Auto-approve modes: AFK/YOLO sessions and session-wide cached approvals skip the modal entirely.
Frequently Asked Questions
How does Kimi CLI associate an approval request with the correct tool call?
The Approval.request() method retrieves the active tool call context via get_current_tool_call_or_none, which is stored in a ContextVar. This ensures each ApprovalRequestRecord is linked to the specific call that initiated it.
What happens when a user chooses "Approve for session"?
The runtime adds the tool action name to self._state.auto_approve_actions. Subsequent tool approval requests for that same action are automatically approved without displaying the UI modal.
Where is the pending request state held while waiting for user input?
Pending requests are stored in the ApprovalRuntime instance defined in src/kimi_cli/approval_runtime/runtime.py. The runtime maintains an internal registry of request records and blocked waiter coroutines until resolve() is invoked.
Which message types travel between the runtime and the UI?
The wire layer in src/kimi_cli/wire/types.py defines ApprovalRequest messages for outbound requests and ApprovalResponse messages for inbound user decisions. These types enable the bidirectional flow between the Python backend and the React frontend.
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 →