From 1d691332e791745db8a98926e776239254b11c52 Mon Sep 17 00:00:00 2001 From: yishuiliunian Date: Thu, 11 Jun 2026 22:24:42 +0800 Subject: [PATCH] fix(acp): advertise permissionModes capability + handle set_permission_mode Advertise loopal's 3 permission modes (bypass/ask_dangerous/ask_any_write) in the initialize response's agentsmeshExtensions so the AgentsMesh permission selector renders loopal's own modes instead of leaking Claude Code's 4-mode set. Map the set_permission_mode control subtype to PermissionModeSwitch; the runtime path (input_control_config -> PermissionMode::from_str) is already wired. --- .../loopal-acp/src/adapter/control_command.rs | 8 +++ crates/loopal-acp/src/adapter/lifecycle.rs | 49 ++++++++++++++----- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/crates/loopal-acp/src/adapter/control_command.rs b/crates/loopal-acp/src/adapter/control_command.rs index f5a5ce36..6875e2a0 100644 --- a/crates/loopal-acp/src/adapter/control_command.rs +++ b/crates/loopal-acp/src/adapter/control_command.rs @@ -51,6 +51,9 @@ pub(crate) fn parse_loopal_control(subtype: &str, p: &Value) -> Option Some(C::GoalUserReopen), "loopal.goalClear" => Some(C::GoalClear), "set_model" => p["model"].as_str().map(|s| C::ModelSwitch(s.to_string())), + "set_permission_mode" => p["mode"] + .as_str() + .map(|s| C::PermissionModeSwitch(s.to_string())), _ => None, } } @@ -78,6 +81,10 @@ mod tests { parse_loopal_control("loopal.mode", &json!({"mode":"plan"})), Some(ControlCommand::ModeSwitch(AgentMode::Plan)) )); + assert!(matches!( + parse_loopal_control("set_permission_mode", &json!({"mode":"ask_dangerous"})), + Some(ControlCommand::PermissionModeSwitch(m)) if m == "ask_dangerous" + )); } #[test] @@ -105,5 +112,6 @@ mod tests { assert!(parse_loopal_control("loopal.bogus", &json!({})).is_none()); assert!(parse_loopal_control("loopal.bgTaskKill", &json!({})).is_none()); assert!(parse_loopal_control("loopal.mode", &json!({"mode":"bogus"})).is_none()); + assert!(parse_loopal_control("set_permission_mode", &json!({})).is_none()); } } diff --git a/crates/loopal-acp/src/adapter/lifecycle.rs b/crates/loopal-acp/src/adapter/lifecycle.rs index c97b4b41..6092fa5b 100644 --- a/crates/loopal-acp/src/adapter/lifecycle.rs +++ b/crates/loopal-acp/src/adapter/lifecycle.rs @@ -6,20 +6,29 @@ use tracing::info; use crate::adapter::AcpAdapter; use crate::types::make_init_response; +/// `initialize` result + AgentsMesh extensions: `controlRequest` (runner routes +/// control-panel actions via `session/control_request`) and `permissionModes` +/// (AgentsMesh selector renders loopal's modes, not Claude Code's). +/// `permissionModes` must match `loopal_tool_api::PermissionMode` (SSOT). +fn init_response_with_extensions() -> Value { + let mut result = serde_json::to_value(make_init_response()).unwrap_or_default(); + if let Some(obj) = result.as_object_mut() { + obj.insert( + "agentsmeshExtensions".into(), + serde_json::json!({ + "controlRequest": true, + "permissionModes": ["bypass", "ask_dangerous", "ask_any_write"], + }), + ); + } + result +} + impl AcpAdapter { - /// Handle `initialize` — return agent capabilities and info. Advertises - /// the AgentsMesh `controlRequest` extension so the runner routes Loopal - /// control-panel actions (bg-task kill / cron delete) via - /// `session/control_request`. pub(crate) async fn handle_initialize(&self, id: i64, _params: Value) { - let mut result = serde_json::to_value(make_init_response()).unwrap_or_default(); - if let Some(obj) = result.as_object_mut() { - obj.insert( - "agentsmeshExtensions".into(), - serde_json::json!({ "controlRequest": true }), - ); - } - self.acp_out.respond(id, result).await; + self.acp_out + .respond(id, init_response_with_extensions()) + .await; info!("ACP initialized"); } @@ -29,3 +38,19 @@ impl AcpAdapter { self.acp_out.respond(id, serde_json::json!({})).await; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn init_response_advertises_control_and_permission_modes() { + let resp = init_response_with_extensions(); + let ext = &resp["agentsmeshExtensions"]; + assert_eq!(ext["controlRequest"], serde_json::json!(true)); + assert_eq!( + ext["permissionModes"], + serde_json::json!(["bypass", "ask_dangerous", "ask_any_write"]) + ); + } +}