feat: ACP providers for claude code and codex (#6605)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user