Skip to content

feat: ACP providers for claude code and codex#6605

Draft
codefromthecrypt wants to merge 1 commit intomainfrom
acole/acp-provider
Draft

feat: ACP providers for claude code and codex#6605
codefromthecrypt wants to merge 1 commit intomainfrom
acole/acp-provider

Conversation

@codefromthecrypt
Copy link
Collaborator

@codefromthecrypt codefromthecrypt commented Jan 21, 2026

Summary

Adds AcpProvider, a Provider implementation that connects to ACP agents over stdio transport. This lets Goose use any ACP-compatible agent (claude-code-acp, codex-acp, etc.) as a provider with full support for MCP extensions, permissions, model listing, and model switching.

  • AcpProvider in crates/goose/src/acp/provider.rs handles the ACP client protocol: session/new, session/prompt, session/set_model, notifications, and permission requests.
  • claude_code_acp.rs and codex_acp.rs wire up the concrete provider definitions with mode mapping and permission option IDs.
  • new_session() returns SessionModelState from the agent's session/new response, enabling model listing and switching through the existing Provider trait.
  • set_model() sends session/set_model via UntypedMessage (sacp doesn't have typed support yet).
  • fetch_supported_models() creates a session to retrieve available models from the ACP agent.
  • Provider-level integration tests in goose-acp/tests/provider_test.rs exercise the full Client→Provider→Server→OpenAI stack in-process using duplex streams.

Type of Change

  • Feature
  • Tests

AI Assistance

  • This PR was created or reviewed with AI assistance

Testing

$ cargo test -p goose-acp -- --nocapture
running 16 tests
test server::tests::test_outcome_to_confirmation::cancelled_maps_to_cancel ... ok
test server::tests::test_outcome_to_confirmation::allow_always_maps_to_always_allow ... ok
test server::tests::test_outcome_to_confirmation::reject_always_maps_to_always_deny ... ok
test server::tests::test_outcome_to_confirmation::reject_once_maps_to_deny_once ... ok
test server::tests::test_outcome_to_confirmation::allow_once_maps_to_allow_once ... ok
test server::tests::test_outcome_to_confirmation::unknown_option_maps_to_cancel ... ok
test server::tests::test_build_model_state::empty_model_list ... ok
test server::tests::test_build_model_state::current_model_reflects_switched_model ... ok
test server::tests::test_build_model_state::returns_current_and_available_models ... ok
test server::tests::test_build_model_state::fetch_error_propagates ... ok
test server::tests::test_format_tool_name_without_extension ... ok
test server::tests::test_format_tool_name_with_extension ... ok
test server::tests::test_read_resource_link_non_file_scheme ... ok
test server::tests::test_mcp_server_to_extension_config ... ok (3 cases)

test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

running 9 tests (provider_test)
test test_provider_model_list ... ok
test test_provider_prompt_basic ... ok
test test_provider_config_mcp ... ok
test test_provider_prompt_mcp ... ok
test test_provider_prompt_image ... ok
test test_provider_model_set ... ok
test test_provider_permission_persistence ... ok
test test_provider_prompt_codemode ... ok
test test_server_basic_completion_via_provider_suite ... ok

test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

running 10 tests (server_test)
test test_initialize_without_provider ... ok
test test_model_list ... ok
test test_prompt_mcp ... ok
test test_prompt_image ... ok
test test_config_mcp ... ok
test test_permission_persistence ... ok
test test_prompt_codemode ... ok
test test_prompt_basic ... ok
test test_load_model ... ok
test test_model_set ... ok

test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
$ cargo test --test providers -- test_claude_code_acp test_codex_acp --nocapture
running 2 tests
=== codex-acp::model_listing ===
[crates/goose/tests/providers.rs:317:9] &models = [
    "gpt-5.3-codex/low",
    "gpt-5.3-codex/medium",
    "gpt-5.3-codex/high",
    "gpt-5.3-codex/xhigh",
    "gpt-5.2-codex/low",
    "gpt-5.2-codex/medium",
    "gpt-5.2-codex/high",
    "gpt-5.2-codex/xhigh",
    "gpt-5.1-codex-max/low",
    "gpt-5.1-codex-max/medium",
    "gpt-5.1-codex-max/high",
    "gpt-5.1-codex-max/xhigh",
    "gpt-5.2/low",
    "gpt-5.2/medium",
    "gpt-5.2/high",
    "gpt-5.2/xhigh",
    "gpt-5.1-codex-mini/medium",
    "gpt-5.1-codex-mini/high",
]
===================
=== claude-code-acp::model_listing ===
[crates/goose/tests/providers.rs:317:9] &models = [
    "default",
    "sonnet",
    "haiku",
]
===================
=== codex-acp::basic_response === Hello!
=== claude-code-acp::basic_response === Hello!
=== codex-acp::tool_usage === test-uuid-12345-67890
=== claude-code-acp::tool_usage === test-uuid-12345-67890
=== codex-acp::image_content === the image is a white background with black text centered: "hello goose!" on the first line and "this is a test image." on the second line.
=== codex-acp::context_length_exceeded_error ===
[crates/goose/tests/providers.rs:236:9] &result = Err(
    ContextLengthExceeded(
        "Request failed: Internal error: {...\"context_window_exceeded\"...}",
    ),
)
===================
test test_codex_acp_provider ... ok
=== claude-code-acp::image_content === the image is a simple white background with black text that reads:
"hello goose! this is a test image."
=== claude-code-acp::model_switch (default -> sonnet) === Hello!
=== claude-code-acp::context_length_exceeded_error ===
[crates/goose/tests/providers.rs:236:9] &result = Err(
    ContextLengthExceeded(
        "Request failed: Internal error: Prompt is too long",
    ),
)
===================
test test_claude_code_acp_provider ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 16 filtered out; finished in 24.78s


============== Providers ==============
✅ claude-code-acp
✅ codex-acp
=======================================

Claude Code ACP

$ GOOSE_PROVIDER=claude-code-acp GOOSE_MODEL=default target/release/goose run \
  --with-extension 'MY_TEST_VAR=hello_from_goose npx -y @modelcontextprotocol/server-everything' \
  -t 'Use the get-env tool and show me the value of MY_TEST_VAR'
TODO
$ GOOSE_PROVIDER=claude-code-acp GOOSE_MODEL=sonnet target/release/goose run \
  --with-streamable-http-extension 'https://mcp.kiwi.com' \
  -t 'Use kiwi to find the fastest itinerary from BKI to SYD tomorrow'
TODO

Codex ACP

$ GOOSE_PROVIDER=codex-acp GOOSE_MODEL=default target/release/goose run \
  --with-extension 'MY_TEST_VAR=hello_from_goose npx -y @modelcontextprotocol/server-everything' \
  -t 'Use the get-env tool and show me the value of MY_TEST_VAR'
TODO
$ GOOSE_PROVIDER=codex-acp target/release/goose run \
  --with-streamable-http-extension 'https://mcp.kiwi.com' \
  -t 'Use kiwi to find the fastest itinerary from BKI to SYD tomorrow'
TODO

Related Issues

Relates to #6605

Screenshots/Demos (for UX changes)

N/A

@codefromthecrypt
Copy link
Collaborator Author

codefromthecrypt commented Jan 21, 2026

@michaelneale @baxen @alexhancock I may be out of time this week and I wanted this to be a collaborative thing anyway. if either of you want to work on this branch it would be gratefully accepted.

The main thing I found is that I can't really test claude because I downgraded my subscription and so get a wait for problem later.

I started testing codex, and first ran into the npx wrapper not working out, so was doign a pattern to cache the rust binary to run directly, similar to how zed works around the same issue.

Hopefully somethjign like this works in the future for claude and codex and when things work well enough we can kill the. CLI providers.
GOOSE_MODE=auto ./target/release/goose run --provider codex-acp --model default -t "hello from codex acp smoke"

Anyway I'm done for at least a day I think so it is safe to work on if one feels like it.

@michaelneale
Copy link
Collaborator

nice - tested with claude and codex - did see odd artifacts like:

I'll list the files in the current working directory.
─── Terminal |  ──────────────────────────

-32002: Tool 'Terminal' not found
─── `ls -la` |  ──────────────────────────
command: ls -la
description: List files in current directory

-32002: Tool '`ls -la`' not foundThe current directory contains a Rust-based project (likely Goose

but it did work and call things - nice!

@michaelneale
Copy link
Collaborator

tagging @DOsinga as this adds a crate etc, but seems to work nice. this will replace the CLI providers.

@alexhancock
Copy link
Collaborator

Nice!

In https://discord.com/channels/1287729918100246654/1408153538537721966/1463661278202695754 I had moved the ACP server code to the goose-acp crate as it seemed very closely related conceptually and added less overhead. Seemed reasonable?

I love the idea of having ACP wrappers for Claude Code and Codex but wonder if there are ones available already we could use? cc @benbrandt

@codefromthecrypt
Copy link
Collaborator Author

@alexhancock on

I love the idea of having ACP wrappers for Claude Code and Codex but wonder if there are ones available already we could use? cc @benbrandt

So, this uses the same wrappers as zed uses and maintains

I ran into a problem using the npm launched in codex-acp, remember codex-acp is actually a rust binary. Then I inspected the zed code and found they are extracting it from the github repo. So, we are doing the same more or less here.

One thing to follow-up on is once the ACP registry is live, this code will be a lot easier.

If your question is about us needed mappings, that's because this change is about goose -> acp and we are not making a large change to goose itself. So we need to adapt our concepts of approval mode etc to these providers who are not 100pct consistent. that's because ACP is designed to propagate approval and mode choices all the way to the client. Currently Goose CLI, UI etc is not designed to work with third party modes. Hence, we have some glue that we need until that's the case.

OTOH for clients calling into goose, nothing changed here. This PR is about adapting our provider infrastructure so that we can have an option besides completely bespoke "CLI provider" glue.

Hope these pointers help!

p.s. goose is lacking modularity for shared packages which is why there's crate re-org. If there was a goose-api crate, and types like provider etc were there, we'd not have the dependency cycle issues which forced into goose-acp-server and goose-acp-client. Since goose isn't used as a library formally anyway, this is tech debt we can clean up and prevent most things getting stuck in goose crate for the same reason.

@codefromthecrypt
Copy link
Collaborator Author

rebase of this is a bear due to intentional session-id tightening. I got it.

TL;DR; since ACP agents own the sessions, we need to rejig a bit to not fight over the ID.

Right now ACP spec does not allow BYO id you have to get one back from the agent. correlation mapping is not great because ops like terminal cross processes and that would force us to add an external id into the DB. The other way is to rejig so that initial session ID creation algo can be overridden based on the provider type, defaulting to normal session manager. So, for ACP, we create a session then get its id and insert it into the normal session manager. That allows us to have 1-1 correlation of session ID. Next is how to do cold resume of session... depending on agent it is load or resume and there is nuance on how to handle this, but we have the code to handle both. If/when ACP changes the session new/load/resume we can replumb, but anyway while what I describe sounds creaky it isn't more troublesome than permission managing.

I have a plan for this and the next push will implement it and update the desc with a diagram how.

@benbrandt
Copy link

Yes we manage the claude and codex wrappers. Though for clarity:

  • claude-code-acp is a wrapper around the claude-agent-sdk, which vendors in a version of claude code and spawns it as a subprocess and their sdk manages that
  • codex-acp is not a "wrapper" but rather a separate binary that pulls a (minimal) fork of codex we use as a Rust dependency. I keep the fork fairly up to date, but just good to note it is a single binary, and unlike claude, doesn't use codex as a subprocess

Regarding codex's npx usage... I worked with another team to add npm support. We don't use it ourselves, but I would hope it works. If it needs to be fixed, and you can provide more information, we can also do that so you can use that if you'd rather

@codefromthecrypt
Copy link
Collaborator Author

@benbrandt on codex-acp I ran into unexplained unresponsiveness, which could be due to internal bootstrapping perhaps. I tried several times despite claude-code-acp working initially. Noting that and that I'm basically working way too much every day I decided to not try to trace the process to figure out where it is glitching, especially as the direction of the registry is download links, also noticing zed only ever used the actual binary. So, basically YMMV on that but being already behind and ACP work due to some engineering and lovely stuff like this.. I'll just whistle by the npm bootstrapper glitch. hope it makes sense! https://github.com/agentclientprotocol/registry/blob/main/codex-acp/agent.json

@codefromthecrypt codefromthecrypt force-pushed the acole/acp-provider branch 2 times, most recently from 0c4bc41 to 505e47e Compare January 27, 2026 08:42
@codefromthecrypt
Copy link
Collaborator Author

update on this. This branch is really expensive to maintain, particularly as there are so many model gaps between the provider design and ACP sessions. Each time I touch this I lose several hours and it still isn't a great bridge between provider and ACP.

I'd like folks to pipe in if this should ship as-is or continue at all. I think Provider -> ACP is really awkward and the days spent on this direction while instructive could have been better spent making a whole new API backend.

.await
}

pub async fn create_session_with_id(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

provider -> acp since there is no way to "bring your own ID" means that we should accept the ACP agent's view of the ID. in order to resolve the chicken egg, we need to be able to accept an external ID or correlate with one. This change prefers to accept it, so that session_id is coherent, and also that future change for load/resume/list sessions will all have the same ID.

self.core.stream(session_id, system, messages, tools).await
}

async fn create_session(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are some copy/paste cruft to buff out into the base type, but I don't want to invest any more hours on this until I've a sense if we think bolting more and more things on the "provider" type is the right way out. IMHO there shuld be an api crate for v2 types and we can clean up all the things like the session chicken egg in new types and migrate to cleaner types. that would also rid the awkward 2 crates for ACP which we only have due to cyclic type dependencies.

use crate::providers::base::{MessageStream, PermissionRouting, ProviderUsage, Usage};
use crate::providers::errors::ProviderError;

pub struct AcpProviderCore {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this type is only here because we don't have an api crate and depdency cycles abound. I think our normal way would be to fold everything into the goose module because otherwise tangles like this trying to decouple..

this stuff is one of the biggest reasons an api crate would help even if provider wasn't awkward to adapt: we end up stuffing everything into the goose crate to avoid cycles

@codefromthecrypt
Copy link
Collaborator Author

I'm going to flatten this stuff into the goose crate as working around it is a cure worse than the disease. that we had a goose-acp crate at all only worked for the server side as it did't need access to goose core types, which are mixed with impl and cause a lot of tension that results in a bit of a mess. best path out is to just keep adding things to goose core crate until we work out modularity as this isn't about ACP rather ACP is just another example of the same tension, which is inability to implement a provider neatly except inside goose crate.

@codefromthecrypt
Copy link
Collaborator Author

#6803 is merged with most of the test refactor stuff, but I will wait until #6832 is in to rebase as that will take the cruft out of defining ACP providers and together will make this PR readable.

@codefromthecrypt
Copy link
Collaborator Author

once this is in probably for real rebase. this PR levels non-acp claude and codex and what they need to stitch MCP through. this will make the ACP variants more straightforward to compare as they are very similar to CLI providers #6972

For this reason I've been toying with us calling the formerly unqualified Provider "LLM Provider" and either CLI or ACP as "Agentic Provider"

@codefromthecrypt
Copy link
Collaborator Author

I've had something personal come up, I've pushed the latest code I have and if someone can/desires to take this over please do. I updated the PR desc.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://block.github.io/goose/pr-preview/pr-6605/

Built to branch gh-pages at 2026-02-12 05:23 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Add Provider implementation that connects to ACP agents (claude-code-acp,
codex-acp) over stdio transport with support for MCP extensions, permissions,
model listing, and model switching.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants