acpbox 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. acpbox-0.1.1/.dockerignore +13 -0
  2. acpbox-0.1.1/.env.example +28 -0
  3. acpbox-0.1.1/.github/workflows/publish-pypi.yaml +78 -0
  4. acpbox-0.1.1/.github/workflows/tag-on-merge.yaml +84 -0
  5. acpbox-0.1.1/.github/workflows/tests-on-pr.yaml +34 -0
  6. acpbox-0.1.1/.gitignore +11 -0
  7. acpbox-0.1.1/Dockerfile +53 -0
  8. acpbox-0.1.1/LICENSE +21 -0
  9. acpbox-0.1.1/PKG-INFO +113 -0
  10. acpbox-0.1.1/README.md +91 -0
  11. acpbox-0.1.1/acpbox/__init__.py +1 -0
  12. acpbox-0.1.1/acpbox/acp_stdio.py +486 -0
  13. acpbox-0.1.1/acpbox/cli.py +7 -0
  14. acpbox-0.1.1/acpbox/config.py +112 -0
  15. acpbox-0.1.1/acpbox/errors.py +25 -0
  16. acpbox-0.1.1/acpbox/main.py +119 -0
  17. acpbox-0.1.1/acpbox/mapping.py +412 -0
  18. acpbox-0.1.1/acpbox/routes/__init__.py +1 -0
  19. acpbox-0.1.1/acpbox/routes/chat.py +195 -0
  20. acpbox-0.1.1/acpbox/routes/models.py +58 -0
  21. acpbox-0.1.1/acpbox/routes/responses.py +101 -0
  22. acpbox-0.1.1/acpbox/schemas.py +134 -0
  23. acpbox-0.1.1/acpbox/session_store.py +40 -0
  24. acpbox-0.1.1/acpbox.egg-info/PKG-INFO +113 -0
  25. acpbox-0.1.1/acpbox.egg-info/SOURCES.txt +50 -0
  26. acpbox-0.1.1/acpbox.egg-info/dependency_links.txt +1 -0
  27. acpbox-0.1.1/acpbox.egg-info/entry_points.txt +2 -0
  28. acpbox-0.1.1/acpbox.egg-info/requires.txt +12 -0
  29. acpbox-0.1.1/acpbox.egg-info/top_level.txt +1 -0
  30. acpbox-0.1.1/config.example.yaml +16 -0
  31. acpbox-0.1.1/docker-compose.yaml +34 -0
  32. acpbox-0.1.1/docs/acp-lifecycle.md +32 -0
  33. acpbox-0.1.1/docs/api-mapping.md +48 -0
  34. acpbox-0.1.1/docs/config.md +75 -0
  35. acpbox-0.1.1/docs/deployment.md +114 -0
  36. acpbox-0.1.1/docs/spec.md +37 -0
  37. acpbox-0.1.1/pyproject.toml +38 -0
  38. acpbox-0.1.1/requirements-dev.txt +1 -0
  39. acpbox-0.1.1/requirements.txt +9 -0
  40. acpbox-0.1.1/setup.cfg +4 -0
  41. acpbox-0.1.1/tests/__init__.py +0 -0
  42. acpbox-0.1.1/tests/conftest.py +81 -0
  43. acpbox-0.1.1/tests/test_chat.py +257 -0
  44. acpbox-0.1.1/tests/test_models.py +46 -0
  45. acpbox-0.1.1/tests/test_responses.py +77 -0
  46. acpbox-0.1.1/tests/test_sessions.py +27 -0
  47. acpbox-0.1.1/tests/unit/__init__.py +0 -0
  48. acpbox-0.1.1/tests/unit/test_config.py +45 -0
  49. acpbox-0.1.1/tests/unit/test_errors.py +35 -0
  50. acpbox-0.1.1/tests/unit/test_mapping.py +273 -0
  51. acpbox-0.1.1/tests/unit/test_session_store.py +38 -0
  52. acpbox-0.1.1/workspace/.gitignore +2 -0
@@ -0,0 +1,13 @@
1
+ .git
2
+ docs/
3
+
4
+ __pycache__
5
+ *.pyc
6
+ .venv
7
+ .pytest_cache
8
+ .mypy_cache
9
+ config.yaml
10
+ .env
11
+ build/
12
+ dist/
13
+ .coverage
@@ -0,0 +1,28 @@
1
+ # Gateway config via environment. Copy to .env and set values.
2
+ # All options can be set here; they override config.yaml.
3
+ # ACP agent runs per-request over stdio (no HTTP, no /ping).
4
+
5
+ # Docker image build only. Comma-separated: opencode, cursor (case-insensitive).
6
+ # Examples: opencode | cursor | opencode,cursor
7
+ # Match ACP_COMMAND below to an installed binary, e.g. ["opencode","acp"] or ["agent","acp"].
8
+ AGENTS=opencode
9
+
10
+ CONFIG_PATH=config.yaml
11
+
12
+ # ACP agent command (stdio). JSON array, e.g. ["opencode", "acp"]
13
+ ACP_COMMAND=["opencode", "acp"]
14
+ # Extra env for ACP process as JSON object
15
+ ACP_ENV={}
16
+ # ACP session/new project directory (relative to process cwd or absolute). Default ./workspace; Docker image and compose use /workspace (see Dockerfile, docker-compose.yaml).
17
+ ACP_WORKSPACE=./workspace
18
+
19
+ GATEWAY_HOST=0.0.0.0
20
+ GATEWAY_PORT=8080
21
+ # Uvicorn process workers (one ACP subprocess per worker). Not OS threads.
22
+ GATEWAY_WORKERS=1
23
+ # Only has effect if uvicorn.run supports threads= (current releases usually do not).
24
+ GATEWAY_THREADS=1
25
+
26
+ # Agent API keys
27
+ # CURSOR_API_KEY=sk-xxx
28
+ # ANTHROPIC_API_KEY=sk-xxx
@@ -0,0 +1,78 @@
1
+ # Build and publish acpbox to PyPI when a release tag is pushed.
2
+ # Expects tags vMAJOR.MINOR.PATCH (same shape as tag-on-merge.yaml).
3
+ # Version in artifacts matches the tag (v stripped). Configure PyPI Trusted Publishing
4
+ # for this repository (publish workflow permission id-token write is required).
5
+
6
+ name: Publish to PyPI
7
+
8
+ on:
9
+ workflow_dispatch:
10
+ workflow_call:
11
+ push:
12
+ tags:
13
+ - "v*"
14
+
15
+ defaults:
16
+ run:
17
+ shell: bash
18
+
19
+ permissions:
20
+ contents: read
21
+ id-token: write
22
+
23
+ jobs:
24
+ publish:
25
+ name: Build and upload to PyPI
26
+ runs-on: ubuntu-latest
27
+
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+ with:
32
+ fetch-depth: 0
33
+
34
+ - name: Resolve release version from tag
35
+ id: rel
36
+ run: |
37
+ TAG="${GITHUB_REF_NAME}"
38
+ VERSION="${TAG#v}"
39
+ if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
40
+ echo "Release tag must look like vMAJOR.MINOR.PATCH, got: $TAG"
41
+ exit 1
42
+ fi
43
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
44
+ echo "Publishing acpbox $VERSION (tag $TAG)"
45
+
46
+ - name: Ensure tag points to main
47
+ run: |
48
+ git fetch origin main
49
+ if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/main; then
50
+ echo "Tagged commit is not on main, refusing to publish."
51
+ exit 1
52
+ fi
53
+
54
+ - name: Set up Python
55
+ uses: actions/setup-python@v5
56
+ with:
57
+ python-version: "3.11"
58
+
59
+ - name: Install build tooling
60
+ run: |
61
+ python -m pip install --upgrade pip
62
+ pip install "build>=1.0"
63
+
64
+ - name: Run tests
65
+ run: |
66
+ pip install -e ".[dev]"
67
+ pytest tests/ -v -m "not integration" --tb=short
68
+
69
+ - name: Build distributions
70
+ env:
71
+ SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.rel.outputs.version }}
72
+ run: python -m build
73
+
74
+ - name: Publish to PyPI
75
+ uses: pypa/gh-action-pypi-publish@release/v1
76
+ with:
77
+ packages-dir: dist/
78
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,84 @@
1
+ # Tag a new SemVer version when a PR is merged into the default branch (not on direct push to main).
2
+ # Starts from v0.1.0 if no tags exist; bumps patch (e.g. v0.1.0 -> v0.1.1) on each merge.
3
+
4
+ name: Tag release on merge
5
+
6
+ on:
7
+ pull_request:
8
+ types: [closed]
9
+ branches:
10
+ - main
11
+ workflow_dispatch:
12
+
13
+ defaults:
14
+ run:
15
+ shell: bash
16
+
17
+ jobs:
18
+ tag-version:
19
+ name: Bump and push version tag
20
+ runs-on: ubuntu-latest
21
+ if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
22
+ permissions:
23
+ contents: write
24
+ actions: write
25
+
26
+ steps:
27
+ - name: Checkout repository
28
+ uses: actions/checkout@v4
29
+ with:
30
+ fetch-depth: 0
31
+ ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || github.event.pull_request.merge_commit_sha }}
32
+
33
+ - name: Fetch all tags
34
+ run: git fetch --tags
35
+
36
+ - name: Check if commit already has a tag
37
+ id: check
38
+ run: |
39
+ if [ -n "$(git tag --points-at HEAD)" ]; then
40
+ echo "skip=true" >> "$GITHUB_OUTPUT"
41
+ echo "Commit already has a tag, skipping."
42
+ else
43
+ echo "skip=false" >> "$GITHUB_OUTPUT"
44
+ fi
45
+
46
+ - name: Get latest SemVer tag and bump patch
47
+ id: version
48
+ if: steps.check.outputs.skip != 'true'
49
+ run: |
50
+ set -e
51
+ # Latest tag matching v* (SemVer), version-sorted; default v0.1.0
52
+ LATEST=$(git tag -l 'v*' | sort -V | tail -1 || true)
53
+ if [ -z "$LATEST" ]; then
54
+ LATEST="v0.1.0"
55
+ fi
56
+ VERSION="${LATEST#v}"
57
+ # Bump patch: 0.1.0 -> 0.1.1
58
+ NEW_VERSION=$(echo "$VERSION" | awk -F. '{$3=$3+1; OFS="."; print $1,$2,$3}')
59
+ echo "previous=$LATEST" >> "$GITHUB_OUTPUT"
60
+ echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
61
+ echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT"
62
+ echo "Bumping $LATEST -> v${NEW_VERSION}"
63
+
64
+ - name: Create and push tag
65
+ if: steps.check.outputs.skip != 'true'
66
+ run: |
67
+ git config user.name "github-actions[bot]"
68
+ git config user.email "github-actions[bot]@users.noreply.github.com"
69
+ TAG="${{ steps.version.outputs.tag }}"
70
+ git tag -a "$TAG" -m "Release $TAG (auto-tag on merge)"
71
+ git push origin "$TAG"
72
+
73
+ - name: Trigger Docker build
74
+ if: steps.check.outputs.skip != 'true'
75
+ uses: actions/github-script@v7
76
+ with:
77
+ script: |
78
+ await github.rest.actions.createWorkflowDispatch({
79
+ owner: context.repo.owner,
80
+ repo: context.repo.repo,
81
+ workflow_id: 'docker-build-push.yaml',
82
+ ref: 'main',
83
+ inputs: { tag: '${{ steps.version.outputs.tag }}' }
84
+ });
@@ -0,0 +1,34 @@
1
+ # Run tests on every pull request (opened, updated, reopened).
2
+
3
+ name: Tests on PR
4
+
5
+ on:
6
+ pull_request:
7
+ types: [opened, synchronize, reopened]
8
+ workflow_dispatch:
9
+
10
+ defaults:
11
+ run:
12
+ shell: bash
13
+
14
+ jobs:
15
+ test:
16
+ name: Test suite
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - name: Checkout repository
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.11"
27
+
28
+ - name: Install dependencies
29
+ run: |
30
+ python -m pip install --upgrade pip
31
+ pip install -e ".[dev]"
32
+
33
+ - name: Run tests
34
+ run: pytest tests/ -v -m "not integration" --cov=acpbox --cov-report=term-missing
@@ -0,0 +1,11 @@
1
+ __pycache__
2
+ *.pyc
3
+ *.egg-info
4
+ .venv
5
+ .pytest_cache
6
+ .mypy_cache
7
+ config.yaml
8
+ .env
9
+ build/
10
+ dist/
11
+ .coverage
@@ -0,0 +1,53 @@
1
+ FROM python:3.11-slim
2
+
3
+ # Comma-separated, case-insensitive: opencode, cursor. Pass at build time (e.g. docker compose build.args from .env).
4
+ ARG AGENTS=
5
+
6
+ RUN groupadd --gid 1000 user \
7
+ && useradd --uid 1000 --gid 1000 --create-home --home-dir /home/user user
8
+
9
+ WORKDIR /app
10
+
11
+ ENV DEBIAN_FRONTEND=noninteractive
12
+ ENV HOME=/home/user
13
+ ENV PATH="/home/user/.local/bin:/home/user/.opencode/bin:${PATH}"
14
+
15
+ RUN set -eux; \
16
+ agents="$(printf '%s' "${AGENTS}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"; \
17
+ agents=",${agents},"; \
18
+ install_opencode=0; \
19
+ install_cursor=0; \
20
+ case "${agents}" in (*,opencode,*) install_opencode=1 ;; esac; \
21
+ case "${agents}" in (*,cursor,*) install_cursor=1 ;; esac; \
22
+ if [ "${install_opencode}" = "0" ] && [ "${install_cursor}" = "0" ]; then \
23
+ echo "AGENTS build-arg empty or without opencode|cursor; image contains acpbox only. Install an ACP agent in a derived image or mount a binary."; \
24
+ else \
25
+ apt-get update; \
26
+ apt-get install -y --no-install-recommends curl ca-certificates bash; \
27
+ if [ "${install_opencode}" = "1" ]; then \
28
+ su user -s /bin/bash -c 'curl -fsSL https://opencode.ai/install | bash'; \
29
+ su user -s /bin/bash -c 'command -v opencode'; \
30
+ fi; \
31
+ if [ "${install_cursor}" = "1" ]; then \
32
+ su user -s /bin/bash -c 'curl -fsSL https://cursor.com/install | bash'; \
33
+ su user -s /bin/bash -c 'command -v agent'; \
34
+ fi; \
35
+ apt-get purge -y curl; \
36
+ apt-get autoremove -y; \
37
+ rm -rf /var/lib/apt/lists/*; \
38
+ fi
39
+
40
+ COPY . .
41
+ RUN pip install --no-cache-dir . \
42
+ && chown -R user:user /app \
43
+ && mkdir -p /workspace \
44
+ && chown user:user /workspace
45
+
46
+ ENV ACP_WORKSPACE=/workspace
47
+ ENV CONFIG_PATH=/app/config.yaml
48
+
49
+ EXPOSE 8080
50
+
51
+ USER user
52
+
53
+ CMD ["acpbox"]
acpbox-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pavel Rykov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
acpbox-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: acpbox
3
+ Version: 0.1.1
4
+ Summary: OpenAI-compatible HTTP API gateway for Agent Client Protocol (ACP) over stdio
5
+ Author: acpbox contributors
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: fastapi>=0.115.0
11
+ Requires-Dist: uvicorn[standard]>=0.32.0
12
+ Requires-Dist: httpx>=0.27.0
13
+ Requires-Dist: pydantic>=2.0
14
+ Requires-Dist: pydantic-settings>=2.0
15
+ Requires-Dist: pyyaml>=6.0
16
+ Requires-Dist: agent-client-protocol>=0.8.1
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8.0; extra == "dev"
19
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
20
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # ACPBox
24
+
25
+ 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.
26
+
27
+ ## Problem
28
+
29
+ Many tools and SDKs expect an OpenAI-style API. ACP agents (e.g. [OpenCode](https://github.com/sst/opencode) via `opencode acp`, or **Cursor Agent** via `agent acp`) speak the [Agent Client Protocol](https://agentclientprotocol.com) 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.
30
+
31
+ ## How it works
32
+
33
+ 1. **Config** – YAML and env define the agent command, env vars, and **workspace** (`ACP_WORKSPACE`, default `./workspace`; Docker `/workspace`) passed as ACP `session/new` `cwd`.
34
+ 2. **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).
35
+ 3. **Reuse per request** – Each request in that worker uses the same process: `session/new` -> optional `session/set_mode` -> `session/prompt`, then the response is returned. The process is not terminated after each request.
36
+ 4. **Translation** – OpenAI requests are converted to ACP JSON-RPC; ACP content (e.g. `session/update` agent_message_chunk) is converted back to OpenAI chat/responses format.
37
+ - `GET /v1/models` – Uses the worker's ACP process: `initialize` (once) and `session/new`, returns **modes** (`modes.availableModes[].id`, e.g. OpenCode's `plan`, `build`) as the list of models.
38
+ - `POST /v1/chat/completions` – Uses the worker's ACP process; `model` selects the ACP mode. Reply as chat completion, or **Server-Sent Events** when `"stream": true` (OpenAI-style `chat.completion.chunk` lines and `data: [DONE]`). With `stream: false`, optional **`acp`** is `{ "steps": [ reasoning + command summaries ] }` (no raw chunks). With `stream: true`, each chunk may still carry raw **`acp`** from the wire (see `docs/api-mapping.md`).
39
+ - `POST /v1/responses` – Same; optional `chat_id` for client-side continuity (non-streaming only); **`acp.steps`** when present, like chat.
40
+
41
+ See [docs/spec.md](docs/spec.md) and [docs/agent-client-protocol/docs/protocol/transports.mdx](docs/agent-client-protocol/docs/protocol/transports.mdx) for details.
42
+
43
+ ```mermaid
44
+ sequenceDiagram
45
+ participant Client
46
+ participant Gateway
47
+ participant AgentProcess
48
+
49
+ Note over Gateway,AgentProcess: One ACP process per uvicorn worker (started in lifespan)
50
+ Client->>Gateway: GET /v1/models
51
+ Gateway->>AgentProcess: initialize, session/new (reuse process)
52
+ AgentProcess-->>Gateway: modes (e.g. plan, build)
53
+ Gateway-->>Client: list of models
54
+
55
+ Client->>Gateway: POST /v1/chat/completions
56
+ Gateway->>AgentProcess: session/new, session/prompt (same process)
57
+ AgentProcess-->>Gateway: session/update, session/prompt result
58
+ Gateway-->>Client: chat completion
59
+ ```
60
+
61
+ ## Quick setup
62
+
63
+ 1. **Config** – Copy `config.example.yaml` to `config.yaml` and adjust. Every option can also be set via environment (see `.env.example`).
64
+
65
+ 2. **Env** – Copy `.env.example` to `.env` and set values. All options (`CONFIG_PATH`, `ACP_*`, `GATEWAY_*`) can be configured via env.
66
+
67
+ **Agent command (OpenCode vs Cursor)** – set `acp.command` in `config.yaml` or `ACP_COMMAND` as a JSON array of strings.
68
+
69
+ | Backend | `config.yaml` | Shell (env) |
70
+ |---------|---------------|-------------|
71
+ | OpenCode | `command: ["opencode", "acp"]` | `export ACP_COMMAND='["opencode","acp"]'` |
72
+ | Cursor Agent | `command: ["agent", "acp"]` | `export ACP_COMMAND='["agent","acp"]'` |
73
+
74
+ 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.
75
+
76
+ 3. **Run** – Install the package (adds the **`acpbox`** CLI). The process manager is **uvicorn** (see `pyproject.toml`). Use **`gateway.workers`** in YAML or **`GATEWAY_WORKERS`** in the environment for process count (one ACP subprocess per worker). **`GATEWAY_THREADS`** is forwarded into **`uvicorn.run`** only if your installed uvicorn exposes a matching `threads=` argument (current releases usually do not; ASGI uses **asyncio** per process).
77
+
78
+ ```bash
79
+ pip install .
80
+ CONFIG_PATH=config.yaml acpbox
81
+ # Or from a checkout without installing the script:
82
+ pip install -r requirements.txt
83
+ CONFIG_PATH=config.yaml python -m acpbox.main
84
+ ```
85
+
86
+ Or with Docker Compose (reads `.env` and runs the **`acpbox`** service). Set **`AGENTS`** in `.env` for the image build (comma-separated `opencode`, `cursor`); the compose file passes it as a build-arg. After changing `AGENTS`, run `docker compose build --no-cache acpbox` so installers run again. Runtime **`ACP_COMMAND`** must match the installed binary (see Agent command table above). The image **CMD** is **`acpbox`** (uvicorn inside **`acpbox.main.run`**), with **`GATEWAY_WORKERS`** and **`GATEWAY_THREADS`** passed through the environment.
87
+
88
+ 4. **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.
89
+
90
+ ## Tests
91
+
92
+ 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`. Unit tests for mapping, errors, session_store, config, and stdio client in `tests/unit/`. Fixtures in `tests/conftest.py`.
93
+
94
+ From repo root:
95
+
96
+ ```bash
97
+ pip install -e ".[dev]"
98
+ pytest tests/ -v
99
+ ```
100
+
101
+ ## Adding your own ACP in Docker
102
+
103
+ Build 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 **`GATEWAY_WORKERS=8`** (or **`gateway.workers`** in YAML). See [docs/deployment.md](docs/deployment.md).
104
+
105
+ ## Specifications
106
+
107
+ - [docs/spec.md](docs/spec.md) – This gateway: OpenAI HTTP API to Agent Client Protocol (stdio).
108
+ - [OpenAI API OpenAPI spec](https://github.com/openai/openai-openapi/tree/manual_spec) – OpenAI REST API specification (OpenAPI).
109
+ - [Agent Client Protocol](https://agentclientprotocol.com) – Protocol for agent-client communication over stdio (JSON-RPC); see `docs/agent-client-protocol/`.
110
+
111
+ ## License
112
+
113
+ This project is licensed under the MIT License, see the [LICENSE](LICENSE) file in the repository root for details.
acpbox-0.1.1/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # ACPBox
2
+
3
+ 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.
4
+
5
+ ## Problem
6
+
7
+ Many tools and SDKs expect an OpenAI-style API. ACP agents (e.g. [OpenCode](https://github.com/sst/opencode) via `opencode acp`, or **Cursor Agent** via `agent acp`) speak the [Agent Client Protocol](https://agentclientprotocol.com) 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.
8
+
9
+ ## How it works
10
+
11
+ 1. **Config** – YAML and env define the agent command, env vars, and **workspace** (`ACP_WORKSPACE`, default `./workspace`; Docker `/workspace`) passed as ACP `session/new` `cwd`.
12
+ 2. **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).
13
+ 3. **Reuse per request** – Each request in that worker uses the same process: `session/new` -> optional `session/set_mode` -> `session/prompt`, then the response is returned. The process is not terminated after each request.
14
+ 4. **Translation** – OpenAI requests are converted to ACP JSON-RPC; ACP content (e.g. `session/update` agent_message_chunk) is converted back to OpenAI chat/responses format.
15
+ - `GET /v1/models` – Uses the worker's ACP process: `initialize` (once) and `session/new`, returns **modes** (`modes.availableModes[].id`, e.g. OpenCode's `plan`, `build`) as the list of models.
16
+ - `POST /v1/chat/completions` – Uses the worker's ACP process; `model` selects the ACP mode. Reply as chat completion, or **Server-Sent Events** when `"stream": true` (OpenAI-style `chat.completion.chunk` lines and `data: [DONE]`). With `stream: false`, optional **`acp`** is `{ "steps": [ reasoning + command summaries ] }` (no raw chunks). With `stream: true`, each chunk may still carry raw **`acp`** from the wire (see `docs/api-mapping.md`).
17
+ - `POST /v1/responses` – Same; optional `chat_id` for client-side continuity (non-streaming only); **`acp.steps`** when present, like chat.
18
+
19
+ See [docs/spec.md](docs/spec.md) and [docs/agent-client-protocol/docs/protocol/transports.mdx](docs/agent-client-protocol/docs/protocol/transports.mdx) for details.
20
+
21
+ ```mermaid
22
+ sequenceDiagram
23
+ participant Client
24
+ participant Gateway
25
+ participant AgentProcess
26
+
27
+ Note over Gateway,AgentProcess: One ACP process per uvicorn worker (started in lifespan)
28
+ Client->>Gateway: GET /v1/models
29
+ Gateway->>AgentProcess: initialize, session/new (reuse process)
30
+ AgentProcess-->>Gateway: modes (e.g. plan, build)
31
+ Gateway-->>Client: list of models
32
+
33
+ Client->>Gateway: POST /v1/chat/completions
34
+ Gateway->>AgentProcess: session/new, session/prompt (same process)
35
+ AgentProcess-->>Gateway: session/update, session/prompt result
36
+ Gateway-->>Client: chat completion
37
+ ```
38
+
39
+ ## Quick setup
40
+
41
+ 1. **Config** – Copy `config.example.yaml` to `config.yaml` and adjust. Every option can also be set via environment (see `.env.example`).
42
+
43
+ 2. **Env** – Copy `.env.example` to `.env` and set values. All options (`CONFIG_PATH`, `ACP_*`, `GATEWAY_*`) can be configured via env.
44
+
45
+ **Agent command (OpenCode vs Cursor)** – set `acp.command` in `config.yaml` or `ACP_COMMAND` as a JSON array of strings.
46
+
47
+ | Backend | `config.yaml` | Shell (env) |
48
+ |---------|---------------|-------------|
49
+ | OpenCode | `command: ["opencode", "acp"]` | `export ACP_COMMAND='["opencode","acp"]'` |
50
+ | Cursor Agent | `command: ["agent", "acp"]` | `export ACP_COMMAND='["agent","acp"]'` |
51
+
52
+ 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.
53
+
54
+ 3. **Run** – Install the package (adds the **`acpbox`** CLI). The process manager is **uvicorn** (see `pyproject.toml`). Use **`gateway.workers`** in YAML or **`GATEWAY_WORKERS`** in the environment for process count (one ACP subprocess per worker). **`GATEWAY_THREADS`** is forwarded into **`uvicorn.run`** only if your installed uvicorn exposes a matching `threads=` argument (current releases usually do not; ASGI uses **asyncio** per process).
55
+
56
+ ```bash
57
+ pip install .
58
+ CONFIG_PATH=config.yaml acpbox
59
+ # Or from a checkout without installing the script:
60
+ pip install -r requirements.txt
61
+ CONFIG_PATH=config.yaml python -m acpbox.main
62
+ ```
63
+
64
+ Or with Docker Compose (reads `.env` and runs the **`acpbox`** service). Set **`AGENTS`** in `.env` for the image build (comma-separated `opencode`, `cursor`); the compose file passes it as a build-arg. After changing `AGENTS`, run `docker compose build --no-cache acpbox` so installers run again. Runtime **`ACP_COMMAND`** must match the installed binary (see Agent command table above). The image **CMD** is **`acpbox`** (uvicorn inside **`acpbox.main.run`**), with **`GATEWAY_WORKERS`** and **`GATEWAY_THREADS`** passed through the environment.
65
+
66
+ 4. **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.
67
+
68
+ ## Tests
69
+
70
+ 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`. Unit tests for mapping, errors, session_store, config, and stdio client in `tests/unit/`. Fixtures in `tests/conftest.py`.
71
+
72
+ From repo root:
73
+
74
+ ```bash
75
+ pip install -e ".[dev]"
76
+ pytest tests/ -v
77
+ ```
78
+
79
+ ## Adding your own ACP in Docker
80
+
81
+ Build 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 **`GATEWAY_WORKERS=8`** (or **`gateway.workers`** in YAML). See [docs/deployment.md](docs/deployment.md).
82
+
83
+ ## Specifications
84
+
85
+ - [docs/spec.md](docs/spec.md) – This gateway: OpenAI HTTP API to Agent Client Protocol (stdio).
86
+ - [OpenAI API OpenAPI spec](https://github.com/openai/openai-openapi/tree/manual_spec) – OpenAI REST API specification (OpenAPI).
87
+ - [Agent Client Protocol](https://agentclientprotocol.com) – Protocol for agent-client communication over stdio (JSON-RPC); see `docs/agent-client-protocol/`.
88
+
89
+ ## License
90
+
91
+ This project is licensed under the MIT License, see the [LICENSE](LICENSE) file in the repository root for details.
@@ -0,0 +1 @@
1
+ # ACP OpenAI API Gateway