feat: add copilot-acp provider (#8154)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
use anyhow::Result;
|
||||
use futures::future::BoxFuture;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::acp::{
|
||||
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, PermissionMapping,
|
||||
ACP_CURRENT_MODEL,
|
||||
};
|
||||
use crate::config::search_path::SearchPaths;
|
||||
use crate::config::{Config, GooseMode};
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::base::{ProviderDef, ProviderMetadata};
|
||||
|
||||
const COPILOT_ACP_PROVIDER_NAME: &str = "copilot-acp";
|
||||
const COPILOT_ACP_DOC_URL: &str = "https://github.com/github/copilot-cli";
|
||||
const COPILOT_ACP_BINARY: &str = "copilot";
|
||||
|
||||
const MODE_AGENT: &str = "https://agentclientprotocol.com/protocol/session-modes#agent";
|
||||
const MODE_PLAN: &str = "https://agentclientprotocol.com/protocol/session-modes#plan";
|
||||
|
||||
pub struct CopilotAcpProvider;
|
||||
|
||||
impl ProviderDef for CopilotAcpProvider {
|
||||
type Provider = AcpProvider;
|
||||
|
||||
fn metadata() -> ProviderMetadata {
|
||||
ProviderMetadata::new(
|
||||
COPILOT_ACP_PROVIDER_NAME,
|
||||
"GitHub Copilot CLI (ACP)",
|
||||
"Use goose with your GitHub Copilot subscription via the Copilot CLI.",
|
||||
ACP_CURRENT_MODEL,
|
||||
vec![],
|
||||
COPILOT_ACP_DOC_URL,
|
||||
vec![],
|
||||
)
|
||||
.with_setup_steps(vec![
|
||||
"Install the Copilot CLI: `npm install -g @github/copilot`",
|
||||
"Run `copilot login` to authenticate with your GitHub account",
|
||||
"Set in your goose config file (`~/.config/goose/config.yaml` on macOS/Linux):\n GOOSE_PROVIDER: copilot-acp\n GOOSE_MODEL: current",
|
||||
"Restart goose for changes to take effect",
|
||||
])
|
||||
}
|
||||
|
||||
fn from_env(
|
||||
model: ModelConfig,
|
||||
extensions: Vec<crate::config::ExtensionConfig>,
|
||||
) -> BoxFuture<'static, Result<AcpProvider>> {
|
||||
Box::pin(async move {
|
||||
let config = Config::global();
|
||||
// with_npm() includes npm global bin dir (desktop app PATH may not)
|
||||
let resolved_command = SearchPaths::builder()
|
||||
.with_npm()
|
||||
.resolve(COPILOT_ACP_BINARY)?;
|
||||
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
|
||||
// Copilot uses standard ACP permission option IDs (allow_once,
|
||||
// allow_always, reject_once) so kind-based fallback handles them.
|
||||
let permission_mapping = PermissionMapping::default();
|
||||
|
||||
let mut args = vec!["--acp".to_string()];
|
||||
if model.model_name != ACP_CURRENT_MODEL {
|
||||
args.push("--model".to_string());
|
||||
args.push(model.model_name.clone());
|
||||
}
|
||||
|
||||
// Copilot modes are full protocol URIs.
|
||||
// No approve-specific mode; permissions are handled separately.
|
||||
let mode_mapping = HashMap::from([
|
||||
(GooseMode::Auto, MODE_AGENT.to_string()),
|
||||
(GooseMode::Approve, MODE_AGENT.to_string()),
|
||||
(GooseMode::SmartApprove, MODE_AGENT.to_string()),
|
||||
(GooseMode::Chat, MODE_PLAN.to_string()),
|
||||
]);
|
||||
|
||||
let provider_config = AcpProviderConfig {
|
||||
command: resolved_command,
|
||||
args,
|
||||
env: vec![],
|
||||
env_remove: vec![],
|
||||
work_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
mcp_servers: extension_configs_to_mcp_servers(&extensions),
|
||||
session_mode_id: Some(mode_mapping[&goose_mode].clone()),
|
||||
mode_mapping,
|
||||
permission_mapping,
|
||||
notification_callback: None,
|
||||
};
|
||||
|
||||
let metadata = Self::metadata();
|
||||
AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use super::{
|
||||
claude_code::ClaudeCodeProvider,
|
||||
codex::CodexProvider,
|
||||
codex_acp::CodexAcpProvider,
|
||||
copilot_acp::CopilotAcpProvider,
|
||||
cursor_agent::CursorAgentProvider,
|
||||
databricks::DatabricksProvider,
|
||||
gcpvertexai::GcpVertexAIProvider,
|
||||
@@ -59,6 +60,7 @@ async fn init_registry() -> RwLock<ProviderRegistry> {
|
||||
registry.register::<ClaudeAcpProvider>(false);
|
||||
registry.register::<ClaudeCodeProvider>(true);
|
||||
registry.register::<CodexAcpProvider>(false);
|
||||
registry.register::<CopilotAcpProvider>(false);
|
||||
registry.register::<CodexProvider>(true);
|
||||
registry.register::<CursorAgentProvider>(false);
|
||||
registry.register::<DatabricksProvider>(true);
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod claude_code;
|
||||
pub(crate) mod cli_common;
|
||||
pub mod codex;
|
||||
pub mod codex_acp;
|
||||
pub mod copilot_acp;
|
||||
pub mod cursor_agent;
|
||||
pub mod databricks;
|
||||
pub mod embedding;
|
||||
|
||||
@@ -121,6 +121,7 @@ struct ProviderTestConfig {
|
||||
test_permissions: bool,
|
||||
test_smart_approve: bool,
|
||||
test_mode_update: bool,
|
||||
test_mcp_tools: bool,
|
||||
test_context_length_exceeded: bool,
|
||||
expect_context_length_exceeded: bool,
|
||||
context_length_exceeded: usize,
|
||||
@@ -144,6 +145,7 @@ impl ProviderTestConfig {
|
||||
test_permissions: true,
|
||||
test_smart_approve: true,
|
||||
test_mode_update: true,
|
||||
test_mcp_tools: true,
|
||||
test_context_length_exceeded: true,
|
||||
expect_context_length_exceeded: true,
|
||||
context_length_exceeded: 600_000,
|
||||
@@ -175,6 +177,11 @@ impl ProviderTestConfig {
|
||||
self
|
||||
}
|
||||
|
||||
fn test_mcp_tools(mut self, v: bool) -> Self {
|
||||
self.test_mcp_tools = v;
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_context_length_exceeded(mut self, v: bool) -> Self {
|
||||
self.expect_context_length_exceeded = v;
|
||||
self
|
||||
@@ -657,11 +664,13 @@ async fn test_provider(config: ProviderTestConfig) -> Result<()> {
|
||||
.await?
|
||||
.test_basic_response()
|
||||
.await?;
|
||||
run_test(GooseMode::Auto).await?.test_tool_usage().await?;
|
||||
run_test(GooseMode::Auto)
|
||||
.await?
|
||||
.test_image_content_support()
|
||||
.await?;
|
||||
if config.test_mcp_tools {
|
||||
run_test(GooseMode::Auto).await?.test_tool_usage().await?;
|
||||
run_test(GooseMode::Auto)
|
||||
.await?
|
||||
.test_image_content_support()
|
||||
.await?;
|
||||
}
|
||||
if config.model_switch_name.is_some() {
|
||||
run_test(GooseMode::Auto).await?.test_model_switch().await?;
|
||||
}
|
||||
@@ -892,6 +901,18 @@ async fn test_codex_acp_provider() -> Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
// Requires: npm install -g @github/copilot
|
||||
#[tokio::test]
|
||||
async fn test_copilot_acp_provider() -> Result<()> {
|
||||
ProviderTestConfig::with_agentic_provider("copilot-acp", ACP_CURRENT_MODEL, "copilot")
|
||||
.model_switch_name("gpt-4.1")
|
||||
// Copilot ignores mcpServers passed via session/new
|
||||
// https://github.com/github/copilot-cli/issues/1040
|
||||
.test_mcp_tools(false)
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[ctor::dtor]
|
||||
fn print_test_report() {
|
||||
TEST_REPORT.print_summary();
|
||||
|
||||
Reference in New Issue
Block a user