How an Approval Request Is Projected Onto the Wire Stream in Kimi CLI
TLDR: Kimi CLI projects an approval request onto the Wire stream by emitting a JSON-RPC request message from WireServer._request_approval, which lets UI front-ends display permission prompts without blocking the running agent turn.
When a tool running inside the MoonshotAI/kimi-cli repository requires explicit user consent—such as before executing a shell command—the runtime must surface that prompt while keeping the session stream alive. Understanding how an approval request is projected onto the Wire stream reveals the non-blocking architecture that allows multiple concurrent sub-agent approvals across Web UI, TUI, and other front-ends.
Initiating the Request in Approval.request
The permission flow begins in src/kimi_cli/soul/approval.py. When a tool needs clearance, it invokes Approval.request, which builds an ApprovalRequest object and registers it with the ApprovalRuntime via self._runtime.create_request.
A typical tool-side call looks like this:
# src/kimi_cli/soul/approval.py
result = await approval.request(
sender="shell",
action="run_command",
description="Run `rm -rf /tmp`",
display=display_blocks,
)
if not result:
raise result.rejection_error()
Once the runtime creates and tracks the request, it broadcasts the object onto the session-level hub so the Wire server can intercept it.
Broadcasting via the RootWireHub
The RootWireHub in src/kimi_cli/wire/root_hub.py acts as a session-level broadcast bus. The WireServer listens to this bus through its _root_hub_loop in src/kimi_cli/wire/server.py. Whenever the loop dequeues a message that is an instance of ApprovalRequest, it immediately forwards it.
# src/kimi_cli/wire/server.py
async def _root_hub_loop(self) -> None:
while True:
msg = await self._root_hub_queue.get()
if isinstance(msg, ApprovalRequest):
await self._request_approval(msg) # <-- projects onto Wire
This broadcast mechanism ensures that approval requests from background sub-agents reach the same central projection point.
Projecting the Request Onto the Wire Stream
The projection itself happens inside WireServer._request_approval in src/kimi_cli/wire/server.py. The method stores the request in _pending_requests, uses the request’s own id as the JSON-RPC message identifier, and emits a JSONRPCRequestMessage carrying the ApprovalRequest payload:
# src/kimi_cli/wire/server.py
async def _request_approval(self, request: ApprovalRequest) -> None:
msg_id = request.id # use request id as JSON‑RPC id
self._pending_requests[msg_id] = request
await self._send_msg(JSONRPCRequestMessage(id=msg_id, params=request))
# Do NOT await request.wait() – keep the UI loop non‑blocking
Because _request_approval returns without awaiting the request’s future, the Wire stream remains responsive. UI front-ends receive the JSON-RPC request in real time and can render an approval dialog while the underlying agent turn continues.
Resolving the Client Response
After the user interacts with the front-end dialog—choosing approve, reject, or approve for session—the client returns an ApprovalResponse. The WireServer handles this in _handle_response, correlating the message by its JSON-RPC id against _pending_requests. It then resolves the original ApprovalRequest and notifies the ApprovalRuntime.
# src/kimi_cli/wire/server.py
# Inside a structural pattern match on the pending request
case ApprovalRequest():
if isinstance(msg, JSONRPCErrorResponse):
request.resolve("reject")
else:
result = ApprovalResponse.model_validate(msg.result)
request.resolve(result.response)
self._approval_runtime.resolve(request.id, result.response,
feedback=result.feedback)
Once resolved, the awaiting tool call in approval.py receives the result and either proceeds or raises a rejection error.
Summary
Approval.requestinsrc/kimi_cli/soul/approval.pycreates anApprovalRequestand registers it with the runtime.- The
RootWireHubbroadcasts the request to theWireServer._root_hub_loop. _request_approvalprojects the request onto the Wire stream as aJSONRPCRequestMessageusing the request's ownid.- The emission is non-blocking, so concurrent approvals and streaming can coexist.
_handle_responsecorrelates the client'sApprovalResponseby JSON-RPCid, resolves the pending request, and unlocks the tool.
Frequently Asked Questions
What file initiates an approval request in Kimi CLI?
src/kimi_cli/soul/approval.py defines the Approval.request method. Tools call this method to build an ApprovalRequest object and register it with the ApprovalRuntime.
How does Kimi CLI keep the UI responsive while waiting for user approval?
The projection method _request_approval emits the JSON-RPC message but does not await request.wait(). This non-blocking design keeps the Wire stream alive and allows the UI to process multiple concurrent approval dialogs.
What message format is used to project approvals onto the Wire stream?
Kimi CLI uses a JSONRPCRequestMessage defined in the Wire protocol. The message carries the ApprovalRequest payload in its params field and uses the request's id as the JSON-RPC id for correlation.
How is the client's response matched back to the original approval request?
The WireServer stores each outgoing request in _pending_requests keyed by its id. When the client replies, _handle_response looks up that same id, validates the payload as an ApprovalResponse, and calls request.resolve() to wake the awaiting tool.
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 →