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.request in src/kimi_cli/soul/approval.py creates an ApprovalRequest and registers it with the runtime.
  • The RootWireHub broadcasts the request to the WireServer._root_hub_loop.
  • _request_approval projects the request onto the Wire stream as a JSONRPCRequestMessage using the request's own id.
  • The emission is non-blocking, so concurrent approvals and streaming can coexist.
  • _handle_response correlates the client's ApprovalResponse by JSON-RPC id, 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:

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 →