story-kit: create 91_bug_permissions_dialog_never_triggers_in_web_ui

This commit is contained in:
Dave
2026-02-23 19:52:26 +00:00
parent d1d7ce47ac
commit 7f139917b4

View File

@@ -6,27 +6,68 @@ name: "Permissions dialog never triggers in web UI"
## Description
The web UI permissions dialog has never successfully triggered. Claude Code in `-p` (pipe) mode is missing two critical CLI flags: `--input-format stream-json` (so it reads permission responses from stdin) and `--permission-mode default` (so it actually emits permission_request events instead of auto-approving). Without these, the full permission channel wiring (PTY → WebSocket → React dialog → WebSocket → PTY) is never exercised.
The web UI has a full permission dialog (Chat.tsx lines 848-947) that never triggers. The frontend and WebSocket client code are correct — the problem is entirely server-side. Claude Code in `-p` mode does not emit structured `permission_request` JSON events via stdout. Instead, when permissions are denied, it outputs plain text like "Claude requested permissions to use X, but you haven't granted it yet" in the tool drawer.
The correct mechanism for non-interactive permission handling is the `--permission-prompt-tool` CLI flag, which delegates permission decisions to an MCP tool. The server needs to expose a `prompt_permission` MCP tool that bridges the permission request through to the WebSocket client, where the frontend dialog can handle it.
## Investigation Findings (from spike work)
- `--permission-mode default` forces permission checks but denials come as **text output**, not structured JSON events
- `--input-format stream-json` is for multi-turn message streaming, not permission responses
- The PTY stdin/stdout approach for permission responses does not work in `-p` mode
- `--permission-prompt-tool <mcp_tool>` is the correct CLI mechanism — it calls the named MCP tool when a permission decision is needed, and the tool's return value (approve/deny) controls whether Claude Code proceeds
- The `.claude/settings.json` allow list auto-approves most tools, but `--permission-prompt-tool` overrides this
## How to Reproduce
1. Start the story-kit server
2. Open the web UI and select claude-code-pty as the model
3. Send a message that triggers a tool requiring permission (e.g. "run `ls` in the terminal")
4. Observe that no permission dialog appears — the tool executes automatically
3. Send a message that triggers a tool requiring permission (e.g. "search the web for something")
4. Observe that no permission dialog appears — the tool either auto-approves or the denial appears as text in the tool drawer
## Actual Result
Claude Code auto-approves all tool use. No `permission_request` NDJSON event is emitted, so the frontend dialog never renders.
No permission dialog renders. Permissions are either auto-approved (via allow list) or denied silently as text output.
## Expected Result
A permission dialog should appear in the web UI showing the tool name and input, with Approve/Deny buttons. The user's response should be sent back to Claude Code via stdin.
A full-screen permission dialog appears showing the tool name and input, with Approve/Deny buttons. The user's response flows back to Claude Code, which proceeds or skips accordingly.
## Implementation Approach
### 1. Add `prompt_permission` MCP tool (`server/src/http/mcp.rs`)
- Tool accepts `tool_name` (string) and `tool_input` (object) parameters
- Handler sends the request through a shared channel to the active WebSocket session
- Handler blocks (async) waiting for the user's approve/deny response
- Returns a JSON result that Claude Code interprets as approval or denial
### 2. Add shared permission channel to AppContext (`server/src/http/context.rs`)
- Add a `PermissionForward` struct with request fields + a oneshot response sender
- Add an mpsc channel pair to `AppContext` for forwarding permission requests from MCP handler to WebSocket handler
### 3. Wire channel in server startup (`server/src/main.rs`)
- Create the permission channel pair and pass to AppContext
### 4. Bridge permissions in WebSocket handler (`server/src/http/ws.rs`)
- Replace the PTY-based `perm_req_tx`/`perm_req_rx` flow with the MCP-based shared channel
- On receiving a `PermissionForward` from the channel, send `WsResponse::PermissionRequest` to the client
- On receiving `WsRequest::PermissionResponse` from the client, send the decision back through the oneshot sender
### 5. Update Claude Code spawning (`server/src/llm/providers/claude_code.rs`)
- Add `--permission-prompt-tool mcp__story-kit__prompt_permission` to the CLI args
- Remove `PermissionReqMsg` struct and `permission_tx` parameter (no longer needed — permissions flow through MCP, not PTY)
- Clean up the triple-duplicate `permission_request` match arms (dead code from story 80 merge)
### 6. Clean up chat function (`server/src/llm/chat.rs`)
- Remove `permission_tx` parameter from `chat()` signature (permissions no longer flow through here)
## Acceptance Criteria
- [ ] Add --input-format stream-json to the claude CLI invocation in run_pty_session so Claude Code reads permission responses from stdin
- [ ] Add --permission-mode default to the claude CLI invocation so Claude Code emits permission_request events instead of auto-approving
- [ ] `prompt_permission` MCP tool exists and is callable by Claude Code via `--permission-prompt-tool`
- [ ] Permission requests flow: Claude Code → MCP tool → server channel → WebSocket → frontend dialog
- [ ] Permission responses flow: frontend dialog → WebSocket → server channel → MCP tool return → Claude Code
- [ ] Trigger a tool use via the web UI and confirm the permission dialog appears
- [ ] Approve a permission request and confirm Claude Code proceeds with the tool
- [ ] Deny a permission request and confirm Claude Code skips the tool
- [ ] Old PTY-based permission code (`PermissionReqMsg`, `permission_tx`, triple duplicate handlers) is removed
- [ ] cargo clippy and cargo test pass