diff --git a/.story_kit/work/1_upcoming/91_bug_permissions_dialog_never_triggers_in_web_ui.md b/.story_kit/work/1_upcoming/91_bug_permissions_dialog_never_triggers_in_web_ui.md index 89ee646..473a616 100644 --- a/.story_kit/work/1_upcoming/91_bug_permissions_dialog_never_triggers_in_web_ui.md +++ b/.story_kit/work/1_upcoming/91_bug_permissions_dialog_never_triggers_in_web_ui.md @@ -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 ` 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