199 lines
10 KiB
Markdown
199 lines
10 KiB
Markdown
# Agent CI
|
|
|
|
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.
|
|
|
|
## Architecture
|
|
|
|
The service has one durable execution path:
|
|
|
|
```text
|
|
webhook -> repository -> reducer -> durable task -> worker -> workflow -> integration
|
|
```
|
|
|
|
`engine/reducer.py` is the pure job state machine. `engine/repository.py` applies its transitions
|
|
atomically to SQLite and persists the resulting tasks. `application/worker/` executes those tasks
|
|
and passes an explicit `JobRun` into the functions under `workflows/`. Modules under
|
|
`integrations/` own external effects, while `api/` contains the FastAPI host and routes.
|
|
|
|
Jobs and workflows are immutable snapshots. Workflows return their final comment body directly;
|
|
progress and resource links are emitted as state-machine events through `JobRun`. The `jobs` table
|
|
is the authoritative state snapshot, while `job_events` provides durable idempotency and audit data.
|
|
|
|
## Commands
|
|
|
|
| Location | Command | Behavior |
|
|
| --- | --- | --- |
|
|
| Issue | `/agent plan [message]` | Create and independently review a new canonical plan. |
|
|
| Issue | `/agent discuss <message>` | Resume the newest planner and post its response. |
|
|
| Issue | `/agent implement [message]` | Create, review, and open a PR. A prior plan is optional. |
|
|
| Issue | `/agent iterate [message]` | Revise and review the plan once, unless an agent PR is open or merged. |
|
|
| 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. |
|
|
|
|
The requester must have Gitea `write`, `admin`, or `owner` permission on the repository. Commands
|
|
are durably sequenced when their webhook arrives, then authorized and executed in receive order per
|
|
issue or pull request. Up to `AGENTCI_MAX_CONCURRENT_JOBS` unrelated targets execute concurrently;
|
|
the default is two. Each command gets one Gitea comment, which is reconciled asynchronously through
|
|
queued, running, and terminal states. Deleted comments are rediscovered by their hidden marker or
|
|
recreated.
|
|
|
|
## 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, 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. Log in to the Gitea container registry with a personal access token, then pull the image:
|
|
|
|
```sh
|
|
docker login git.krtss.de
|
|
docker compose pull
|
|
```
|
|
|
|
5. Authenticate the configured OpenCode providers before starting the persistent server:
|
|
|
|
```sh
|
|
docker compose run --rm opencode opencode auth login
|
|
docker compose run --rm opencode opencode auth list
|
|
```
|
|
|
|
6. Start the services:
|
|
|
|
```sh
|
|
docker compose up --no-build -d
|
|
```
|
|
|
|
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.
|
|
|
|
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`.
|
|
|
|
`/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.
|
|
|
|
### Image publishing
|
|
|
|
Every push to `master` runs `.gitea/workflows/publish-image.yaml` and publishes the image as both
|
|
`git.krtss.de/stanponomarev/agentci:latest` and
|
|
`git.krtss.de/stanponomarev/agentci:<full-commit-sha>`. Configure these repository Actions secrets
|
|
before the first run:
|
|
|
|
| Secret | Value |
|
|
| --- | --- |
|
|
| `REGISTRY_USERNAME` | Username that owns the package or can write packages for the owner. |
|
|
| `REGISTRY_TOKEN` | Personal access token with package write permission. |
|
|
|
|
The automatic Gitea Actions token cannot publish packages. Compose uses `latest` by default; set
|
|
`AGENTCI_IMAGE=git.krtss.de/stanponomarev/agentci:<full-commit-sha>` in `.env` to deploy an immutable
|
|
revision.
|
|
|
|
## OpenCode
|
|
|
|
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_RESEARCH_VARIANT`
|
|
configures the research subagent and defaults to `high`. `AGENTCI_EXPLORE_MODEL` and
|
|
`AGENTCI_EXPLORE_VARIANT` configure OpenCode's explore agent and default to
|
|
`openai/gpt-5.6-luna` with `low`.
|
|
|
|
The `AGENTCI_OPENCODE_VERSION` Docker build argument controls the npm version or range installed into
|
|
the image and defaults to `^1`. The build verifies that the resolved version is still OpenCode 1.x
|
|
and prints it. Docker may reuse the cached installation layer until the configured version or build
|
|
inputs change. 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`. OpenCode's default global and built-in-agent
|
|
permission policies remain in effect; Agent CI does not replace them with an allow-all policy.
|
|
Repository-local OpenCode config and external plugins are disabled so a clone cannot replace the
|
|
service policy. OpenCode's default plugins remain enabled for provider authentication. Its 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 can only use Exa web search, Context7, and the `gh_grep` public-code search
|
|
MCP. All filesystem, shell, editing, task, and other tools are denied for that agent. Exa is enabled
|
|
with `OPENCODE_ENABLE_EXA=1`. Agent CI initializes or refreshes CodeGraph before every parent turn
|
|
and locally excludes `.codegraph/` from Git.
|
|
|
|
### Security boundary
|
|
|
|
OpenCode does not provide an OS sandbox. Its default permission system controls agent tools and
|
|
approval requests, while Docker limits what the non-root process can reach. Any shell command that
|
|
OpenCode permits still has the Unix-level access of that container user, so environment filtering
|
|
is only accidental-exposure hygiene and cannot protect readable files from an allowed shell command.
|
|
|
|
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
|
|
AGENTCI_PYTHON_VERSION=3.13
|
|
AGENTCI_DOTNET_CHANNEL=10.0
|
|
```
|
|
|
|
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 shell commands permitted by its active agent policy. See
|
|
`install-scripts/README.md` for the script contract.
|
|
|
|
Agent CI continues to create branches, validate diffs, commit, and push after OpenCode returns. This
|
|
keeps workflow behavior deterministic, but an OpenCode agent with shell permission can still run
|
|
Git commands itself.
|
|
|
|
## State and recovery
|
|
|
|
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.
|
|
|
|
SQLite stores the current job state, an idempotent event inbox, and durable listener tasks. State
|
|
transitions and workflow creation/linking commit atomically; timestamps are storage metadata rather
|
|
than reducer state. Control effects retry with bounded backoff. A delayed authorization blocks later
|
|
workflow execution for the same issue or pull request, but not other targets or later control work.
|
|
Development environment installers remain serialized because they share the persistent tools
|
|
directory.
|
|
|
|
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. A restart before `JobStarted` returns the
|
|
task to the queue; after `JobStarted`, the job is failed, its sessions are aborted, and execution is
|
|
never replayed because a partial model turn may already have changed files. Git pushes are never
|
|
forced.
|
|
|
|
## Development
|
|
|
|
Install and validate with:
|
|
|
|
```sh
|
|
uv sync
|
|
uv run ruff check .
|
|
uv run pyright
|
|
uv run pytest
|
|
uv run pytest --cov=agentci --cov-branch
|
|
docker compose config
|
|
```
|
|
|
|
The coverage command is an opt-in diagnostic report; the regular test run remains the default and
|
|
coverage percentage is not used as a pass threshold.
|
|
|
|
Prompts and JSON schemas live outside Python so orchestration remains focused on execution flow.
|