story-kit: merge 91_bug_permissions_dialog_never_triggers_in_web_ui

This commit is contained in:
Dave
2026-02-23 21:38:45 +00:00
parent 02a1edc3de
commit 3087297b88
6 changed files with 127 additions and 95 deletions

View File

@@ -760,6 +760,24 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
}
}
}
},
{
"name": "prompt_permission",
"description": "Present a permission request to the user via the web UI. Used by Claude Code's --permission-prompt-tool to delegate permission decisions to the frontend dialog. Returns on approval; returns an error on denial.",
"inputSchema": {
"type": "object",
"properties": {
"tool_name": {
"type": "string",
"description": "The tool requesting permission (e.g. 'Bash', 'Write')"
},
"input": {
"type": "object",
"description": "The tool's input arguments"
}
},
"required": ["tool_name", "input"]
}
}
]
}),
@@ -818,6 +836,8 @@ async fn handle_tools_call(
"request_qa" => tool_request_qa(&args, ctx).await,
// Diagnostics
"get_server_logs" => tool_get_server_logs(&args),
// Permission bridge (Claude Code → frontend dialog)
"prompt_permission" => tool_prompt_permission(&args, ctx).await,
_ => Err(format!("Unknown tool: {tool_name}")),
};
@@ -1550,6 +1570,49 @@ fn tool_get_server_logs(args: &Value) -> Result<String, String> {
Ok(recent.join("\n"))
}
/// MCP tool called by Claude Code via `--permission-prompt-tool`.
///
/// Forwards the permission request through the shared channel to the active
/// WebSocket session, which presents a dialog to the user. Blocks until the
/// user approves or denies (with a 5-minute timeout).
async fn tool_prompt_permission(args: &Value, ctx: &AppContext) -> Result<String, String> {
let tool_name = args
.get("tool_name")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let tool_input = args
.get("input")
.cloned()
.unwrap_or(json!({}));
let request_id = uuid::Uuid::new_v4().to_string();
let (response_tx, response_rx) = tokio::sync::oneshot::channel();
ctx.perm_tx
.send(crate::http::context::PermissionForward {
request_id: request_id.clone(),
tool_name: tool_name.clone(),
tool_input,
response_tx,
})
.map_err(|_| "No active WebSocket session to receive permission request".to_string())?;
let approved = tokio::time::timeout(
std::time::Duration::from_secs(300),
response_rx,
)
.await
.map_err(|_| format!("Permission request for '{tool_name}' timed out after 5 minutes"))?
.map_err(|_| "Permission response channel closed unexpectedly".to_string())?;
if approved {
Ok(format!("Permission granted for '{tool_name}'"))
} else {
Err(format!("User denied permission for '{tool_name}'"))
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1647,7 +1710,8 @@ mod tests {
assert!(names.contains(&"move_story_to_merge"));
assert!(names.contains(&"request_qa"));
assert!(names.contains(&"get_server_logs"));
assert_eq!(tools.len(), 27);
assert!(names.contains(&"prompt_permission"));
assert_eq!(tools.len(), 28);
}
#[test]