diff --git a/.story_kit/work/2_current/62_story_allow_frontend_ui_to_accept_permissions_requests.md b/.story_kit/work/2_current/62_story_allow_frontend_ui_to_accept_permissions_requests.md new file mode 100644 index 0000000..c7473f6 --- /dev/null +++ b/.story_kit/work/2_current/62_story_allow_frontend_ui_to_accept_permissions_requests.md @@ -0,0 +1,26 @@ +--- +name: Agent Permission Prompts in Web UI +test_plan: pending +--- + +# Story 62: Agent Permission Prompts in Web UI + +## User Story + +As a user interacting with an agent through the web UI, I want to be prompted for permission approvals (e.g. file writes, commits) so that the agent can complete tasks that require elevated permissions without getting blocked. + +Right now, the web UI does not have any way of the user allowing permissions requests, so dev processes are basically blocked. + +## Acceptance Criteria + +- [ ] When an agent action requires permission (e.g. writing to a file, committing), the web UI surfaces a prompt to the user +- [ ] The user can approve or deny the permission request from the UI +- [ ] On approval, the agent continues with the requested action +- [ ] On denial, the agent receives the denial and adjusts its approach +- [ ] Permission prompts display enough context (file path, action type) for the user to make an informed decision + + +## Out of Scope + +- Bulk/blanket permission grants (e.g. "allow all writes to this directory") +- Persisting permission decisions across sessions diff --git a/server/src/llm/providers/claude_code.rs b/server/src/llm/providers/claude_code.rs index 63a210b..c53b4bf 100644 --- a/server/src/llm/providers/claude_code.rs +++ b/server/src/llm/providers/claude_code.rs @@ -369,6 +369,53 @@ fn run_pty_session( let _ = writeln!(pty_writer, "{}", response); } } + // Claude Code is requesting user approval before executing a tool. + // Forward the request to the async context via permission_tx and + // block until the user responds (or a 5-minute timeout elapses). + "permission_request" => { + let request_id = json + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let tool_name = json + .get("tool_name") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + let tool_input = json + .get("input") + .cloned() + .unwrap_or(serde_json::Value::Object(serde_json::Map::new())); + + if let Some(ref ptx) = permission_tx { + let (resp_tx, resp_rx) = std::sync::mpsc::sync_channel(1); + let _ = ptx.send(PermissionReqMsg { + request_id: request_id.clone(), + tool_name, + tool_input, + response_tx: resp_tx, + }); + // Block until the user responds or a 5-minute timeout elapses. + let approved = resp_rx + .recv_timeout(std::time::Duration::from_secs(300)) + .unwrap_or(false); + let response = serde_json::json!({ + "type": "permission_response", + "id": request_id, + "approved": approved, + }); + let _ = writeln!(pty_writer, "{}", response); + } else { + // No handler configured — deny by default. + let response = serde_json::json!({ + "type": "permission_response", + "id": request_id, + "approved": false, + }); + let _ = writeln!(pty_writer, "{}", response); + } + } _ => {} } }