story-219: add Always Allow button to web UI permission dialog
Cherry-pick from feature branch — code was never squash-merged despite story being accepted (bug 226). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use crate::http::context::AppContext;
|
||||
use crate::http::context::{AppContext, PermissionDecision};
|
||||
use crate::http::workflow::{PipelineState, load_pipeline_state};
|
||||
use crate::io::onboarding;
|
||||
use crate::io::watcher::WatcherEvent;
|
||||
@@ -29,6 +29,8 @@ enum WsRequest {
|
||||
PermissionResponse {
|
||||
request_id: String,
|
||||
approved: bool,
|
||||
#[serde(default)]
|
||||
always_allow: bool,
|
||||
},
|
||||
/// Heartbeat ping from the client. The server responds with `Pong` so the
|
||||
/// client can detect stale (half-closed) connections.
|
||||
@@ -250,7 +252,7 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem
|
||||
// Permission requests arrive from the MCP `prompt_permission` tool via
|
||||
// `ctx.perm_rx` and are forwarded to the client as `PermissionRequest`.
|
||||
// When the client responds, we resolve the corresponding oneshot.
|
||||
let mut pending_perms: HashMap<String, oneshot::Sender<bool>> = HashMap::new();
|
||||
let mut pending_perms: HashMap<String, oneshot::Sender<PermissionDecision>> = HashMap::new();
|
||||
|
||||
loop {
|
||||
// Outer loop: wait for the next WebSocket message.
|
||||
@@ -324,9 +326,16 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc<AppContext>>) -> impl poem
|
||||
// (permission responses and cancellations).
|
||||
Some(Ok(WsMessage::Text(inner_text))) = stream.next() => {
|
||||
match serde_json::from_str::<WsRequest>(&inner_text) {
|
||||
Ok(WsRequest::PermissionResponse { request_id, approved }) => {
|
||||
Ok(WsRequest::PermissionResponse { request_id, approved, always_allow }) => {
|
||||
if let Some(resp_tx) = pending_perms.remove(&request_id) {
|
||||
let _ = resp_tx.send(approved);
|
||||
let decision = if always_allow {
|
||||
PermissionDecision::AlwaysAllow
|
||||
} else if approved {
|
||||
PermissionDecision::Approve
|
||||
} else {
|
||||
PermissionDecision::Deny
|
||||
};
|
||||
let _ = resp_tx.send(decision);
|
||||
}
|
||||
}
|
||||
Ok(WsRequest::Cancel) => {
|
||||
@@ -457,9 +466,11 @@ mod tests {
|
||||
WsRequest::PermissionResponse {
|
||||
request_id,
|
||||
approved,
|
||||
always_allow,
|
||||
} => {
|
||||
assert_eq!(request_id, "req-42");
|
||||
assert!(approved);
|
||||
assert!(!always_allow);
|
||||
}
|
||||
_ => panic!("expected PermissionResponse variant"),
|
||||
}
|
||||
@@ -477,9 +488,34 @@ mod tests {
|
||||
WsRequest::PermissionResponse {
|
||||
request_id,
|
||||
approved,
|
||||
always_allow,
|
||||
} => {
|
||||
assert_eq!(request_id, "req-99");
|
||||
assert!(!approved);
|
||||
assert!(!always_allow);
|
||||
}
|
||||
_ => panic!("expected PermissionResponse variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_permission_response_always_allow() {
|
||||
let json = r#"{
|
||||
"type": "permission_response",
|
||||
"request_id": "req-100",
|
||||
"approved": true,
|
||||
"always_allow": true
|
||||
}"#;
|
||||
let req: WsRequest = serde_json::from_str(json).unwrap();
|
||||
match req {
|
||||
WsRequest::PermissionResponse {
|
||||
request_id,
|
||||
approved,
|
||||
always_allow,
|
||||
} => {
|
||||
assert_eq!(request_id, "req-100");
|
||||
assert!(approved);
|
||||
assert!(always_allow);
|
||||
}
|
||||
_ => panic!("expected PermissionResponse variant"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user