feat: ACP providers for claude code and codex (#6605)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Adrian Cole
2026-03-11 06:55:05 +08:00
committed by GitHub
parent a64269c845
commit 902c2ac28a
22 changed files with 2214 additions and 131 deletions
+91
View File
@@ -0,0 +1,91 @@
use anyhow::Result;
use futures::future::BoxFuture;
use std::path::PathBuf;
use crate::acp::{
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, PermissionMapping,
};
use crate::config::search_path::SearchPaths;
use crate::config::{Config, GooseMode};
use crate::model::ModelConfig;
use crate::providers::base::{ProviderDef, ProviderMetadata};
const CLAUDE_ACP_PROVIDER_NAME: &str = "claude-acp";
pub const CLAUDE_ACP_DEFAULT_MODEL: &str = "default";
const CLAUDE_ACP_DOC_URL: &str = "https://github.com/zed-industries/claude-agent-acp";
const CLAUDE_ACP_BINARY: &str = "claude-agent-acp";
pub struct ClaudeAcpProvider;
impl ProviderDef for ClaudeAcpProvider {
type Provider = AcpProvider;
fn metadata() -> ProviderMetadata {
ProviderMetadata::new(
CLAUDE_ACP_PROVIDER_NAME,
"Claude Code",
"ACP wrapper for Anthropic's Claude. Install: npm install -g @zed-industries/claude-agent-acp",
CLAUDE_ACP_DEFAULT_MODEL,
vec![],
CLAUDE_ACP_DOC_URL,
vec![],
)
}
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(CLAUDE_ACP_BINARY)?;
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
// claude-agent-acp permission option_ids
let permission_mapping = PermissionMapping {
allow_option_id: Some("allow".to_string()),
reject_option_id: Some("reject".to_string()),
rejected_tool_status: sacp::schema::ToolCallStatus::Failed,
};
let provider_config = AcpProviderConfig {
command: resolved_command,
args: vec![],
env: vec![],
// Prevent nested-session detection in claude-agent-acp (wraps Claude Code)
env_remove: vec!["CLAUDECODE".to_string()],
work_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
mcp_servers: extension_configs_to_mcp_servers(&extensions),
session_mode_id: Some(map_goose_mode(goose_mode)),
permission_mapping,
};
let metadata = Self::metadata();
AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await
})
}
}
fn map_goose_mode(goose_mode: GooseMode) -> String {
match goose_mode {
GooseMode::Auto => {
// Closest to "autonomous": Claude Code's bypassPermissions skips confirmations.
"bypassPermissions".to_string()
}
GooseMode::Approve => {
// Claude Code's default matches "ask before risky actions".
"default".to_string()
}
GooseMode::SmartApprove => {
// Best-effort: acceptEdits auto-accepts file edits but still prompts for risky ops.
"acceptEdits".to_string()
}
GooseMode::Chat => {
// Plan mode disables tool execution, aligning with chat-only intent.
"plan".to_string()
}
}
}
+121
View File
@@ -0,0 +1,121 @@
use anyhow::Result;
use futures::future::BoxFuture;
use std::path::PathBuf;
use crate::acp::{
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, PermissionMapping,
};
use crate::config::search_path::SearchPaths;
use crate::config::{Config, GooseMode};
use crate::model::ModelConfig;
use crate::providers::base::{ProviderDef, ProviderMetadata};
const CODEX_ACP_PROVIDER_NAME: &str = "codex-acp";
pub const CODEX_ACP_DEFAULT_MODEL: &str = "gpt-5.2-codex";
const CODEX_ACP_DOC_URL: &str = "https://github.com/zed-industries/codex-acp";
pub struct CodexAcpProvider;
impl ProviderDef for CodexAcpProvider {
type Provider = AcpProvider;
fn metadata() -> ProviderMetadata {
ProviderMetadata::new(
CODEX_ACP_PROVIDER_NAME,
"Codex CLI",
"ACP adapter for OpenAI's coding assistant. Install: npm install -g @zed-industries/codex-acp",
CODEX_ACP_DEFAULT_MODEL,
vec![],
CODEX_ACP_DOC_URL,
vec![],
)
}
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(CODEX_ACP_PROVIDER_NAME)?;
let work_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let env = vec![];
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
let mcp_servers = extension_configs_to_mcp_servers(&extensions);
// fixed goose mode via -c overrides until session/set-mode works
let (approval_policy, sandbox_mode) = map_goose_mode(goose_mode);
let mut args = vec![
"-c".to_string(),
format!("approval_policy={approval_policy}"),
"-c".to_string(),
format!("sandbox_mode={sandbox_mode}"),
];
// Codex sandbox blocks network by default. Enable it when HTTP MCP
// servers are configured so codex-acp can connect to them.
let has_http_mcp = mcp_servers
.iter()
.any(|s| matches!(s, sacp::schema::McpServer::Http(_)));
if has_http_mcp {
args.extend([
"-c".to_string(),
"sandbox_workspace_write.network_access=true".to_string(),
]);
}
// codex-acp permission option_ids
let permission_mapping = PermissionMapping {
allow_option_id: Some("approved".to_string()),
reject_option_id: Some("abort".to_string()),
rejected_tool_status: sacp::schema::ToolCallStatus::Failed,
};
let provider_config = AcpProviderConfig {
command: resolved_command,
args,
env,
env_remove: vec![],
work_dir,
mcp_servers,
// Disabled until https://github.com/zed-industries/codex-acp/issues/179 is fixed.
session_mode_id: None,
permission_mapping,
};
let metadata = Self::metadata();
AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await
})
}
}
// Codex sandbox scope determines what needs approval: operations within the
// sandbox are auto-approved, operations outside it trigger on-request prompts.
// So Approve uses read-only sandbox to force write approvals through goose.
fn map_goose_mode(goose_mode: GooseMode) -> (&'static str, &'static str) {
match goose_mode {
GooseMode::Auto => ("never", "danger-full-access"),
GooseMode::SmartApprove => ("on-request", "workspace-write"),
GooseMode::Approve => ("on-request", "read-only"),
GooseMode::Chat => ("never", "read-only"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[test_case(GooseMode::Auto, "never", "danger-full-access")]
#[test_case(GooseMode::SmartApprove, "on-request", "workspace-write")]
#[test_case(GooseMode::Approve, "on-request", "read-only")]
#[test_case(GooseMode::Chat, "never", "read-only")]
fn test_map_goose_mode(mode: GooseMode, expected_approval: &str, expected_sandbox: &str) {
let (approval, sandbox) = map_goose_mode(mode);
assert_eq!(approval, expected_approval);
assert_eq!(sandbox, expected_sandbox);
}
}
+4
View File
@@ -7,8 +7,10 @@ use super::{
base::{Provider, ProviderMetadata},
bedrock::BedrockProvider,
chatgpt_codex::ChatGptCodexProvider,
claude_acp::ClaudeAcpProvider,
claude_code::ClaudeCodeProvider,
codex::CodexProvider,
codex_acp::CodexAcpProvider,
cursor_agent::CursorAgentProvider,
databricks::DatabricksProvider,
gcpvertexai::GcpVertexAIProvider,
@@ -52,7 +54,9 @@ async fn init_registry() -> RwLock<ProviderRegistry> {
registry.register::<BedrockProvider>(false);
registry.register::<LocalInferenceProvider>(false);
registry.register::<ChatGptCodexProvider>(true);
registry.register::<ClaudeAcpProvider>(false);
registry.register::<ClaudeCodeProvider>(true);
registry.register::<CodexAcpProvider>(false);
registry.register::<CodexProvider>(true);
registry.register::<CursorAgentProvider>(false);
registry.register::<DatabricksProvider>(true);
+2
View File
@@ -9,9 +9,11 @@ pub mod bedrock;
pub mod canonical;
pub mod catalog;
pub mod chatgpt_codex;
pub mod claude_acp;
pub mod claude_code;
pub(crate) mod cli_common;
pub mod codex;
pub mod codex_acp;
pub mod cursor_agent;
pub mod databricks;
pub mod embedding;