feat: switch to opencode

This commit is contained in:
2026-07-21 00:00:24 +02:00
parent 60cb143402
commit 6f24df8cd3
45 changed files with 1288 additions and 757 deletions
+89 -81
View File
@@ -1,8 +1,8 @@
# Agent CI
Agent CI is a private Gitea webhook host that turns issue and pull-request
comments into resumable Codex planning and implementation workflows. It runs as
one persistent Docker Compose service on the same Docker network as Gitea.
Agent CI is a private Gitea webhook host that turns issue and pull-request comments into resumable
OpenCode planning and implementation workflows. Docker Compose runs the webhook worker and a
private OpenCode server on the same Docker network as Gitea.
## Commands
@@ -15,68 +15,89 @@ one persistent Docker Compose service on the same Docker network as Gitea.
| PR | `/agent iterate [message]` | Resume an agent implementation and its reviewer once. |
| PR | `/agent fix [message]` | Start a fresh one-shot fix session and push one commit. |
Anyone who can comment on an issue or pull request can enqueue commands. Each
command gets separate queued and started comments. Final plans, PR results,
failures, and remaining review findings are posted separately.
The requester must have Gitea `write`, `admin`, or `owner` permission on the repository. Each
accepted command gets separate queued and started comments. Final plans, PR results, failures, and
remaining review findings are posted separately.
## Deploy
1. Create a Gitea bot user with repository read/write access and an API token.
2. Copy `.env.example` to `.env` and set the external network and internal
Gitea URL.
3. Create `secrets/gitea_token` containing the bot token and
`secrets/webhook_secret` containing a high-entropy webhook secret.
4. Build and start the service:
2. Copy `.env.example` to `.env` and set the external network, Gitea URL, and provider-qualified
OpenCode models.
3. Create `secrets/gitea_token`, `secrets/webhook_secret`, and
`secrets/opencode_server_password`. Use high-entropy values for both secret/password files.
4. Build the image:
```sh
docker compose up --build -d
./scripts/build.sh
```
5. Authenticate Codex interactively in the persistent container:
5. Authenticate the configured OpenCode providers before starting the persistent server:
```sh
docker compose exec agentci codex login --device-auth
docker compose exec agentci codex login status
docker compose run --rm opencode opencode auth login
docker compose run --rm opencode opencode auth list
```
6. In Gitea, create a JSON webhook targeting
`http://agentci:8080/webhooks/gitea`. Set the same secret and subscribe to
issue comments, PR timeline comments, and PR review comments.
6. Start the services:
`/health/live` reports process health. `/health/ready` returns 503 until Codex
authentication is usable. The worker leaves jobs queued while authentication is
missing.
```sh
docker compose up --no-build -d
```
## Configuration
7. In Gitea, create a JSON webhook targeting `http://agentci:8080/webhooks/gitea`. Set the same
webhook secret and subscribe to issue comments, PR timeline comments, and PR review comments.
Model, reasoning effort, review-pass counts, bot identity, branch prefix, and
turn timeout use `AGENTCI_` environment variables. Defaults are shown in
`.env.example`. Gitea credentials and webhook secrets are intentionally
file-based Compose secrets.
OpenCode caches provider state. After adding or changing authentication on an already running
deployment, run the one-off `auth login` command above and then `docker compose restart opencode`.
Every planning and implementation session can delegate external research to a
read-only `research` subagent. It defaults to `gpt-5.6-luna` with high reasoning
and has public network access, live web search, Context7 documentation lookup,
and `gh_grep` public GitHub code search. Configure its model and effort with
`AGENTCI_RESEARCH_MODEL` and `AGENTCI_RESEARCH_REASONING`. Context7 works
without authentication at lower rate limits; set the optional
`AGENTCI_CONTEXT7_API_KEY` for authenticated usage. The key is passed only to
Codex's Context7 MCP transport and is excluded from agent shell environments.
`/health/live` reports process health. `/health/ready` returns 503 until the OpenCode server is
healthy and every configured model exists, supports tool calls, accepts its configured variant, and
has a connected provider. The worker leaves jobs queued while the runtime is unavailable.
Planning, implementation, and all review sessions also receive the local
CodeGraph MCP server for repository structure, symbol relationships, and change
impact. Agent CI initializes or refreshes the index before every Codex turn and
locally excludes `.codegraph/` from Git. The research subagent intentionally
does not receive CodeGraph.
## OpenCode
### Development environments
Models use OpenCode's `provider/model` format. Planning, implementation, and research can use
different providers. Optional `AGENTCI_PLAN_VARIANT` and `AGENTCI_IMPLEMENT_VARIANT` values are
passed directly to OpenCode for providers that support variants.
`AGENTCI_INSTALL_SCRIPTS` is a comma-delimited ordered list of development
environment installers. The supplied `python` and `dotnet` scripts install only
their runtimes; they are ordinary scripts that can be replaced or removed.
Implementation agents remain responsible for restoring project dependencies
and selecting build/test commands. Configure the supplied scripts with
`AGENTCI_PYTHON_VERSION` and `AGENTCI_DOTNET_CHANNEL`:
`scripts/build.sh` supplies a unique required cache key on every invocation. The image then runs
`npm install -g 'opencode-ai@^1'`, verifies the installed major version, and prints it. Compose also
requests a no-cache build. A direct build without `OPENCODE_REFRESH` fails rather than silently
reusing an old OpenCode installation layer. This is intentionally fresh rather than reproducible.
Runtime auto-update is disabled so an image cannot cross into OpenCode 2.x after it is built.
The trusted configuration is `opencode/opencode.json`. It grants `permission: "allow"` globally
and to every built-in or custom agent that Agent CI can invoke. Repository-local OpenCode config
and plugins are disabled so a clone cannot replace the service policy. OpenCode's scanned home and
global configuration paths are root-owned and read-only, and external skill discovery is disabled,
so an agent cannot persist instructions for later repositories. CodeGraph, Context7, and `gh_grep`
are configured as MCP servers. Set `AGENTCI_CONTEXT7_API_KEY` to raise Context7 rate limits.
The `research` subagent has the same unrestricted permissions as every other agent. Its prompt
asks it to focus on external evidence, but this is guidance rather than an isolation boundary.
Agent CI initializes or refreshes CodeGraph before every turn and locally excludes `.codegraph/`
from Git.
### Security boundary
OpenCode does not provide an OS sandbox. It can execute shell commands, edit Git metadata and
environment files, access external directories, use loopback and network services, bind ports, and
invoke subagents without approval. It can access every shared workspace, development tool, runtime
credential, and mounted file readable by the non-root container user. Environment filtering is
only accidental-exposure hygiene and cannot protect readable files from shell commands.
Docker remains the OS boundary. The services run as non-root without added capabilities,
privileged mode, an unconfined seccomp/AppArmor profile, or a nested `bubblewrap` sandbox. The only
host bind mount is the read-only installer directory; there are no
writable host filesystem mounts. The OpenCode HTTP server is not published to the host, is password
protected, and is reachable by Agent CI over an internal Compose network.
## Development environments
`AGENTCI_INSTALL_SCRIPTS` is a comma-delimited ordered list of development environment installers.
The supplied `python` and `dotnet` scripts install only their runtimes; they can be replaced or
removed. Configure them with `AGENTCI_PYTHON_VERSION` and `AGENTCI_DOTNET_CHANNEL`:
```env
AGENTCI_INSTALL_SCRIPTS=python,dotnet,company-tools
@@ -84,44 +105,29 @@ AGENTCI_PYTHON_VERSION=3.13
AGENTCI_DOTNET_CHANNEL=10.0
```
Every name resolves to an executable file in `install-scripts/`, mounted
read-only at `/etc/agentci/install-scripts`. Names cannot contain paths and
duplicates are rejected. See `install-scripts/README.md` for the script contract.
Every name resolves to a file in `install-scripts/`, mounted read-only at
`/etc/agentci/install-scripts`. Names cannot contain paths and duplicates are rejected. Installers
run after each implementation clone or branch sync and fail the job on an unknown script, timeout,
or non-zero exit. They receive no Agent CI or Gitea secret values in their environment, but remain
trusted operator code. Tools persist under `/var/lib/agentci/dev-tools`, and OpenCode can read or
modify them through its unrestricted shell. See `install-scripts/README.md` for the script contract.
Installers run in order after each implementation clone or branch sync and fail
the job on an unknown script, timeout, or non-zero exit. They receive no AgentCI
or Gitea secret values in their environment, but remain trusted operator code
running as the service user. Tools persist under `/var/lib/agentci/dev-tools`;
its `bin` directory is added to implementation agents' `PATH` with read-only
sandbox access. The agents can use those tools for builds and validation, but
Agent CI does not impose host-side build commands.
Planning/review commands can only read their workflow clone and have no shell
network access. Implementation/fix commands can edit the clone but cannot
modify `.git`; they can reach public internet destinations while private and
loopback destinations remain blocked. Codex's interactive Git trust check is
skipped because every turn runs non-interactively against a service-owned clone;
the configured filesystem and network permissions still apply. Git credentials
exist only in the service-owned clone/push subprocess and are not inherited by
Codex turns. The image and Compose capability/security settings let the non-root
service create Codex's nested `bwrap` sandbox. They follow Codex's secure
devcontainer pattern instead of making the service container privileged. Do not
remove Codex's configured filesystem and network restrictions. In particular,
`systempaths=unconfined` removes Docker's outer masked `/proc` subpaths so the
nested user/PID namespace can mount its own procfs. Runtimes such as CoreCLR
require `/proc/self/maps`; Codex still controls visibility through the fresh
procfs and its filesystem policy.
Agent CI continues to create branches, validate diffs, commit, and push after OpenCode returns. This
keeps workflow behavior deterministic, but unrestricted OpenCode is not prevented from running Git
commands itself.
## State and recovery
The `agentci_data` volume contains SQLite, persistent workflow clones, and
installed development runtimes.
`codex_home` contains login state and resumable Codex sessions. Both are kept
indefinitely and should be backed up together.
The `agentci_data` volume contains SQLite, workflow clones, and installed development runtimes.
The `opencode_home` volume contains provider authentication, OpenCode's database, and resumable
sessions. Tea's Gitea token configuration is regenerated in an ephemeral tmpfs and is not copied to
`opencode_home`. Back up both persistent volumes together.
Queued jobs survive restart. An in-progress job is marked failed after restart
instead of being replayed, because replaying a partially completed model turn
could duplicate changes. Git pushes are never forced.
The OpenCode migration tags existing workflows as Codex-owned and preserves their session IDs for
rollback, but OpenCode refuses to resume them. Follow-up commands against those workflows ask for a
new plan or implementation. Queued jobs survive restart. An in-progress job is aborted in OpenCode
and marked failed instead of being replayed because a partial model turn may already have changed
files. Git pushes are never forced.
## Development
@@ -132,7 +138,9 @@ uv sync
uv run ruff check .
uv run pyright
uv run pytest
docker compose config
./scripts/build.sh
```
The tests fail if any tracked Python file exceeds 250 lines. Prompts and JSON
schemas live outside Python so orchestration modules remain small and readable.
The tests fail if any tracked Python file exceeds 250 lines. Prompts and JSON schemas live outside
Python so orchestration modules remain small and readable.