feat(claude-code): add permission prompt routing for approve mode (#7420)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Adrian Cole
2026-02-23 19:06:07 -05:00
committed by GitHub
parent edd48c7a89
commit e9cef3ebbb
6 changed files with 759 additions and 172 deletions
+111 -5
View File
@@ -1,9 +1,14 @@
use anyhow::Result;
use dotenvy::dotenv;
use futures::StreamExt;
use goose::agents::extension_manager::ExtensionManagerCapabilities;
use goose::agents::{ExtensionManager, GoosePlatform, PromptManager};
use goose::config::ExtensionConfig;
use goose::conversation::message::{Message, MessageContent};
use goose::agents::{
Agent, AgentConfig, AgentEvent, ExtensionManager, GoosePlatform, PromptManager, SessionConfig,
};
use goose::config::{ExtensionConfig, GooseMode, PermissionManager};
use goose::conversation::message::{ActionRequiredData, Message, MessageContent};
use goose::permission::permission_confirmation::PrincipalType;
use goose::permission::{Permission, PermissionConfirmation};
use goose::providers::anthropic::ANTHROPIC_DEFAULT_MODEL;
use goose::providers::azure::AZURE_DEFAULT_MODEL;
use goose::providers::base::Provider;
@@ -19,7 +24,7 @@ use goose::providers::openai::OPEN_AI_DEFAULT_MODEL;
use goose::providers::sagemaker_tgi::SAGEMAKER_TGI_DEFAULT_MODEL;
use goose::providers::snowflake::SNOWFLAKE_DEFAULT_MODEL;
use goose::providers::xai::XAI_DEFAULT_MODEL;
use goose::session::SessionManager;
use goose::session::{SessionManager, SessionType};
use goose_test_support::{ExpectedSessionId, McpFixture, FAKE_CODE, TEST_SESSION_ID};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
@@ -94,6 +99,7 @@ struct ProviderTester {
extension_manager: Arc<ExtensionManager>,
is_cli_provider: bool,
model_switch_name: Option<String>,
mcp_extension: ExtensionConfig,
}
impl ProviderTester {
@@ -103,6 +109,7 @@ impl ProviderTester {
extension_manager: Arc<ExtensionManager>,
is_cli_provider: bool,
model_switch_name: Option<String>,
mcp_extension: ExtensionConfig,
) -> Self {
Self {
provider,
@@ -110,6 +117,7 @@ impl ProviderTester {
extension_manager,
is_cli_provider,
model_switch_name,
mcp_extension,
}
}
@@ -376,6 +384,7 @@ impl ProviderTester {
}
async fn run_test_suite(&self) -> Result<()> {
let _guard = env_lock::lock_env([("GOOSE_MODE", Some("auto"))]);
self.test_model_listing().await?;
self.test_basic_response(&self.session_id_for_test("basic_response"))
.await?;
@@ -393,8 +402,104 @@ impl ProviderTester {
self.test_context_length_exceeded_error(&self.session_id_for_test("context_length"))
.await?;
}
drop(_guard);
// codex: one-shot subprocess, no bidirectional control protocol
if self.name != "codex" {
self.test_permission_allow().await?;
self.test_permission_deny().await?;
}
Ok(())
}
async fn run_permission_test(&self, permission: Permission, label: &str) -> Result<()> {
// Guard must live through agent.reply() — providers read GOOSE_MODE at spawn time.
let _guard = env_lock::lock_env([("GOOSE_MODE", Some("approve"))]);
let provider = if self.is_cli_provider {
create_with_named_model(
&self.name.to_lowercase(),
&self.provider.get_model_config().model_name,
vec![self.mcp_extension.clone()],
)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?
} else {
self.provider.clone()
};
let temp_dir = tempfile::tempdir()?;
let session_manager = Arc::new(SessionManager::new(temp_dir.path().to_path_buf()));
let permission_manager = Arc::new(PermissionManager::new(temp_dir.path().to_path_buf()));
let agent = Agent::with_config(AgentConfig::new(
session_manager.clone(),
permission_manager,
None,
GooseMode::Approve,
true,
GoosePlatform::GooseCli,
));
let session = session_manager
.create_session(
std::env::current_dir()?,
"permission_test".to_string(),
SessionType::User,
)
.await?;
agent.update_provider(provider, &session.id).await?;
agent
.add_extension(self.mcp_extension.clone(), &session.id)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
let message =
Message::user().with_text("Use the get_code tool and output only its result.");
let session_config = SessionConfig {
id: session.id,
schedule_id: None,
max_turns: Some(5),
retry_config: None,
};
let mut stream = agent.reply(message, session_config, None).await?;
let mut saw_action_required = false;
while let Some(event) = stream.next().await {
let event = event?;
if let AgentEvent::Message(ref msg) = event {
for content in &msg.content {
if let MessageContent::ActionRequired(ar) = content {
if let ActionRequiredData::ToolConfirmation { ref id, .. } = ar.data {
saw_action_required = true;
agent
.handle_confirmation(
id.clone(),
PermissionConfirmation {
principal_type: PrincipalType::Tool,
permission: permission.clone(),
},
)
.await;
}
}
}
}
}
assert!(saw_action_required);
println!("=== {}::{} ===", self.name, label);
Ok(())
}
async fn test_permission_allow(&self) -> Result<()> {
self.run_permission_test(Permission::AllowOnce, "permission_allow")
.await
}
async fn test_permission_deny(&self) -> Result<()> {
self.run_permission_test(Permission::DenyOnce, "permission_deny")
.await
}
}
fn load_env() {
@@ -507,7 +612,7 @@ async fn test_provider(
ExtensionManagerCapabilities { mcpui: false },
));
extension_manager
.add_extension(mcp_extension, None, None, None)
.add_extension(mcp_extension.clone(), None, None, None)
.await
.expect("failed to add extension");
@@ -517,6 +622,7 @@ async fn test_provider(
extension_manager,
is_cli_provider,
model_switch_name.map(String::from),
mcp_extension,
);
let _mcp = mcp;
let result = tester.run_test_suite().await;