feat: add copilot-acp provider (#8154)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Adrian Cole
2026-03-30 10:15:11 -04:00
committed by GitHub
parent d9b69ef735
commit 764760edfb
4 changed files with 122 additions and 5 deletions
+93
View File
@@ -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
})
}
}
+2
View File
@@ -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);
+1
View File
@@ -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;
+26 -5
View File
@@ -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();