How the Kimi CLI Approval Runtime Works: Structure, Lifecycle, and Request Projection
The Kimi CLI approval runtime uses the ApprovalRuntime class in src/kimi_cli/approval_runtime/runtime.py to maintain an in-memory ledger of requests, coordinate asynchronous waiters, and project approval events onto the wire protocol for UI rendering.
The MoonshotAI/kimi-cli repository implements a sophisticated approval system that pauses privileged tool execution until explicitly authorized. Understanding the structure of this approval runtime and how requests are projected to the UI layer is essential for developers extending the CLI or building custom integrations.
Core Components of the Approval Runtime
The approval system centers on four primary data structures defined across the runtime and wire protocol modules:
-
ApprovalRuntime(src/kimi_cli/approval_runtime/runtime.py) – The main orchestrator that holds the_requestsdictionary, managesasyncio.Futurewaiters in_waiters, tracks_waiter_counts, and maintains a reference to theRootWireHubfor message projection. -
ApprovalRequestRecord(src/kimi_cli/approval_runtime/models.py) – A dataclass storing complete metadata for each request includingid,tool_call_id,sender,action,description,displayblocks,source, timestamps,status, and the finalresponse. -
ApprovalSource(src/kimi_cli/approval_runtime/models.py) – An enumeration distinguishing request origins as eitherforeground_turn(direct user interaction) orbackground_agent(autonomous agent execution), enabling source-aware cancellation. -
ApprovalRuntimeEvent(src/kimi_cli/approval_runtime/models.py) – A simple wrapper carryingrequest_createdorrequest_resolvedevents to subscriber callbacks. -
Wire Protocol Types (
src/kimi_cli/wire/types.py) – DefinesApprovalRequestandApprovalResponsemessage schemas that serialize internal state for transmission over theRootWireHub.
The Approval Request Lifecycle
The runtime manages requests through five distinct phases, each implemented as specific methods on the ApprovalRuntime class.
1. Creation and Event Publication
When a tool requires authorization, it calls ApprovalRuntime.create_request() to instantiate an ApprovalRequestRecord. The runtime immediately stores this in _requests and triggers two simultaneous actions:
- Publishes a
request_createdevent to all subscribers via_publish_event() - Serializes the request into a wire
ApprovalRequestmessage via_publish_wire_request()for UI consumption
2. Asynchronous Waiting
Consumers awaiting user input call await ApprovalRuntime.wait_for_response(request_id, timeout). The runtime creates a shared asyncio.Future in _waiters for that request ID. Multiple coroutines can await the same future, and the runtime tracks active waiter counts in _waiter_counts to determine when the last observer has timed out.
3. Resolution
Upon user decision, the UI invokes ApprovalRuntime.resolve() with the request ID, response type (approve, approve_for_session, or reject), and optional feedback. The runtime:
- Updates the record status to resolved
- Stores the response and feedback
- Fulfills the future in
_waiters, releasing all waiting coroutines - Fires a
request_resolvedevent - Emits a wire
ApprovalResponsevia_publish_wire_response()
4. Source-Aware Cancellation
If a background agent terminates or a UI session ends, cancel_by_source() iterates through pending records matching that ApprovalSource. It marks matching requests as cancelled, raises ApprovalCancelledError for active waiters, and publishes resolution events with a reject response.
5. Event Subscription
External components register for lifecycle updates via ApprovalRuntime.subscribe(callback). The runtime invokes these callbacks with ApprovalRuntimeEvent objects whenever requests are created or resolved, enabling logging, telemetry, or custom UI reactions without modifying core logic.
How Approval Requests Are Projected
Projection refers to the transformation of internal Python objects into protocol messages that travel over the wire to UI components. This occurs through two private helper methods in src/kimi_cli/approval_runtime/runtime.py:
_publish_wire_request(request) maps the ApprovalRequestRecord fields into a wire.types.ApprovalRequest message containing the ID, tool call ID, sender, action, description, display blocks, and source identifiers. This message routes through the RootWireHub to reach shell UIs or ACP interfaces.
_publish_wire_response(request_id, response, feedback) constructs a wire.types.ApprovalResponse carrying the final decision and optional feedback string. This signals the UI to clear the prompt and update its state.
The UI layer listens for these wire messages, renders the appropriate prompt (e.g., "Delete /tmp/secret.txt?"), and calls back into ApprovalRuntime.resolve() when the user responds, completing the request loop.
Working with the Approval Runtime
The following examples demonstrate common patterns for interacting with the approval system:
from kimi_cli.approval_runtime.runtime import ApprovalRuntime
from kimi_cli.approval_runtime.models import ApprovalSource
# Initialize runtime with reference to the wire hub
approval_runtime = ApprovalRuntime(wire_hub=root_hub)
# Create a request from a privileged tool
request = approval_runtime.create_request(
sender="file_manager",
action="delete_file",
description="Delete /tmp/secret.txt?",
tool_call_id="tool-42",
display=[{"type": "text", "value": "Are you sure?"}],
source=ApprovalSource(kind="foreground_turn", id="turn-1")
)
# Await user decision with timeout handling
from kimi_cli.approval_runtime.errors import ApprovalCancelledError
try:
response, feedback = await approval_runtime.wait_for_response(
request.id,
timeout=30
)
if response == "approve":
# Execute the privileged operation
perform_deletion()
except ApprovalCancelledError:
# Handle cancellation from UI close or agent termination
logger.info("Request was cancelled")
# Manual resolution for background agents
approval_runtime.resolve(
request_id=request.id,
response="reject",
feedback="User policy violation",
approved_via_session_cache=False,
)
# Subscribe to lifecycle events for logging
def log_approval_event(event):
logger.info(f"Approval {event.kind}: {event.request.id}")
token = approval_runtime.subscribe(log_approval_event)
# Later: approval_runtime.unsubscribe(token)
Summary
- Single Source of Truth: All approval state lives in the
ApprovalRuntimeinstance; wire messages are read-only projections. - Async Coordination: The runtime uses shared
asyncio.Futureobjects in_waitersto allow multiple coroutines to await the same user decision without duplication. - Source Tracking:
ApprovalSourceenables bulk cancellation viacancel_by_source()when background agents or UI sessions terminate. - Event-Driven Architecture: Subscribers receive
ApprovalRuntimeEventnotifications forrequest_createdandrequest_resolvedevents, supporting extensible side effects like analytics. - Wire Projection: Private methods
_publish_wire_request()and_publish_wire_response()serialize internal records intowire.typesmessages for UI rendering.
Frequently Asked Questions
What is the difference between ApprovalRequestRecord and the wire ApprovalRequest?
ApprovalRequestRecord is the internal Python dataclass stored in ApprovalRuntime._requests that tracks the full state of a request including timestamps, status, and the final response. The wire ApprovalRequest (defined in src/kimi_cli/wire/types.py) is a serialized message format containing only the fields necessary for UI rendering, such as the description and display blocks. The _publish_wire_request() method transforms the internal record into the wire format for transmission over the RootWireHub.
How does the runtime handle multiple components waiting for the same approval?
The runtime maintains a dictionary of _waiters where each request ID maps to a shared asyncio.Future. When multiple coroutines call wait_for_response() with the same ID, they all receive a reference to the same future object. The _waiter_counts dictionary tracks how many components are awaiting each request, allowing the runtime to clean up resources only when the last waiter has either received a response or timed out.
What happens when a background agent terminates while requests are pending?
The runtime provides cancel_by_source(), which accepts an ApprovalSource parameter identifying the terminated agent. The method iterates through all pending requests in _requests, matches those with the specified source, marks them as cancelled, and raises ApprovalCancelledError for any active futures. It also publishes request_resolved events and wire responses to ensure UIs clear the pending prompts.
Where does the UI send responses back to the runtime?
UI components receive wire ApprovalRequest messages through the RootWireHub (implemented in src/kimi_cli/wire/root_hub.py). When the user makes a decision, the UI calls ApprovalRuntime.resolve() directly with the request ID, response type (approve, approve_for_session, or reject), and optional feedback. This method updates the internal record and fulfills the waiting future, completing the request lifecycle without requiring additional wire protocol messages for the response path.
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 →