OpenAI-compatible HTTP API that acts as a gateway to the Agent Client Protocol (ACP). Clients use the usual OpenAI endpoints (/v1/models, /v1/chat/completions, /v1/responses); the gateway runs the API with uvicorn and keeps one ACP agent process per worker over stdio (JSON-RPC), not HTTP.
Many tools and SDKs expect an OpenAI-style API. ACP agents (e.g. OpenCode via opencode acp, or Cursor Agent via agent acp) speak the Agent Client Protocol over stdin/stdout. This gateway provides a single HTTP entry point: one base URL, OpenAI-shaped API, with one ACP binary instance per uvicorn worker.
- Config – YAML and env define the agent command, env vars, and workspace (
ACPBOX_ACP_WORKSPACE, default./workspace; Docker/workspace) passed as ACPsession/newcwd. - One ACP process per worker – The app runs under uvicorn. In lifespan each worker starts one ACP agent subprocess and keeps it for the worker's lifetime. With 8 workers you get 8 ACP binary instances. ACP uses stdio only (JSON-RPC, newline-delimited).
- Reuse per request – Each request in that worker uses the same process:
session/new-> optionalsession/set_mode->session/prompt, then the response is returned. The process is not terminated after each request. - Translation – OpenAI requests are converted to ACP JSON-RPC; ACP content (e.g.
session/updateagent_message_chunk) is converted back to OpenAI chat/responses format.GET /v1/models– Uses the worker's ACP process:initialize(once) andsession/new, returns modes (modes.availableModes[].id, e.g. OpenCode'splan,build) as the list of models.POST /v1/chat/completions– Uses the worker's ACP process;modelselects the ACP mode. Reply as chat completion, or Server-Sent Events when"stream": true(OpenAI-stylechat.completion.chunklines anddata: [DONE]). Withstream: false, optionalacpis{ "steps": [ reasoning + command summaries ] }(no raw chunks). Withstream: true, each chunk may still carry rawacpfrom the wire (seedocs/api-mapping.md).POST /v1/responses– Same; optionalchat_idfor client-side continuity (non-streaming only);acp.stepswhen present, like chat.GET /v1/agent– Returns info about the current agent (type, config path, known permissions, writable status).GET/PUT/PATCH /v1/agent/permissions– Read and manage agent permissions at runtime. Changes are written to the agent's native config file and apply to all future requests. See docs/api-mapping.md.
See docs/spec.md and docs/agent-client-protocol/docs/protocol/transports.mdx for details.
sequenceDiagram
participant Client
participant Gateway
participant AgentProcess
Note over Gateway,AgentProcess: One ACP process per uvicorn worker (started in lifespan)
Client->>Gateway: GET /v1/models
Gateway->>AgentProcess: initialize, session/new (reuse process)
AgentProcess-->>Gateway: modes (e.g. plan, build)
Gateway-->>Client: list of models
Client->>Gateway: POST /v1/chat/completions
Gateway->>AgentProcess: session/new, session/prompt (same process)
AgentProcess-->>Gateway: session/update, session/prompt result
Gateway-->>Client: chat completion
The package is on PyPI. That installs the acpbox CLI (uvicorn-driven server, see pyproject.toml).
pipx - isolated install, good for a long-lived CLI. You still need a config.yaml (copy config.example.yaml from this repo, or from GitHub).
pipx install acpbox
acpbox --config ./config.yamlUpgrade later with pipx upgrade acpbox. Try once without installing globally:
acpbox --config ./config.yamlpip
pip install acpbox
acpbox --config ./config.yamlGit clone - develop or pin to a revision.
git clone https://github.com/EvilFreelancer/acpbox
cd acpbox
cp config.example.yaml config.yaml
# edit config.yaml
pip install -e .
acpbox --config ./config.yamlFrom a checkout without the console script, after pip install -r requirements.txt:
python -m acpbox.main --config ./config.yaml-
Config – Copy
config.example.yamltoconfig.yamland adjust. Every option can also be set via environment (see.env.example). -
Env – Copy
.env.exampleto.envand set values. All options (ACPBOX_CONFIG_PATH,ACPBOX_ACP_*,ACPBOX_GATEWAY_*) can be configured via env.Agent command (OpenCode vs Cursor) – set
acp.commandinconfig.yamlorACPBOX_ACP_COMMANDas a JSON array of strings.Backend config.yamlShell (env) OpenCode command: ["opencode", "acp"]export ACPBOX_ACP_COMMAND='["opencode","acp"]'Cursor Agent command: ["agent", "acp"]export ACPBOX_ACP_COMMAND='["agent","acp"]'Claude (ACP adapter) command: ["claude-agent-acp"]export ACPBOX_ACP_COMMAND='["claude-agent-acp"]'Codex (ACP adapter) command: ["codex-acp"]export ACPBOX_ACP_COMMAND='["codex-acp"]'Use an absolute path if the binary is not on
PATH(e.g.["/home/you/.local/bin/agent","acp"]). Cursor Agent must be installed and logged in (agent login) so the subprocess can reach your account. -
Run – Start
acpboxwithACPBOX_CONFIG_PATH(and other env vars) as needed. Usegateway.workersin YAML orACPBOX_GATEWAY_WORKERSfor worker count (one ACP subprocess per worker).ACPBOX_GATEWAY_THREADSis forwarded intouvicorn.runonly if your uvicorn supports athreads=argument (many builds do not; ASGI uses asyncio per process).
Or with Docker Compose (reads .env and runs the acpbox service). Set AGENTS in .env to install missing agent binaries at container startup (comma-separated opencode, cursor, claude, codex). Runtime ACPBOX_ACP_COMMAND must match the installed binary (see Agent command table above). The image CMD is acpbox (uvicorn inside acpbox.main.run), with ACPBOX_GATEWAY_WORKERS and ACPBOX_GATEWAY_THREADS passed through the environment.
- Use – Point any OpenAI client at
http://localhost:8080/v1(or your host/port). List models, call chat completions or responses; the gateway translates to ACP and back.
The gateway detects the agent type from acp.command and exposes endpoints to manage its permissions at runtime. Supported agents:
| Agent | Command | Config file | Permission format |
|---|---|---|---|
| OpenCode | opencode acp |
{workspace}/opencode.json |
"permission": {"read": "allow", ...} |
| Claude | claude-agent-acp |
{workspace}/.claude/settings.json |
"permissions": {"allow": [...], "deny": [...]} |
| Codex | codex-acp |
{workspace}/codex.json |
"permission": {...} + approval_mode |
| Cursor | agent acp |
{workspace}/.cursor/settings.json |
"permission": {...} |
Permissions are written to the agent's native config file on disk. Since each request creates a new ACP session (session/new), the agent picks up updated permissions on the next request.
Presets let you flip permissions in bulk: allow_all (full access), deny_all (lock everything down), ask_all (OpenCode only). Combine a preset with explicit overrides to allow only specific tools:
curl -X PUT http://localhost:8080/v1/agent/permissions \
-H 'Content-Type: application/json' \
-d '{"preset": "deny_all", "permissions": {"atlassian_*": "allow"}}'The writable field in responses indicates whether the config file can be written to. If it is read-only, write endpoints return 403.
See docs/api-mapping.md for full endpoint reference.
Tests use a mock ACP over stdio (fake subprocess that responds with JSON-RPC). Route tests: tests/test_models.py, tests/test_chat.py, tests/test_responses.py, tests/test_sessions.py. Agent config tests: tests/test_agent_config.py (adapters, detection, API, writability). Unit tests for mapping, errors, session_store, config, and stdio client in tests/unit/. Fixtures in tests/conftest.py.
From repo root:
pip install -e ".[dev]"
pytest tests/ -vBuild an image that includes acpbox and your ACP agent binary (e.g. opencode acp). Set acp.command and acp.env in config or .env. The server runs with uvicorn via acpbox; each worker starts one ACP process in lifespan. To run 8 ACP instances, set ACPBOX_GATEWAY_WORKERS=8 (or gateway.workers in YAML). See docs/deployment.md.
- docs/spec.md – This gateway: OpenAI HTTP API to Agent Client Protocol (stdio).
- OpenAI API OpenAPI spec – OpenAI REST API specification (OpenAPI).
- Agent Client Protocol – Protocol for agent-client communication over stdio (JSON-RPC); see
docs/agent-client-protocol/.
This project is licensed under the MIT License, see the LICENSE file in the repository root for details.