2026-07-22 23:10:23 +02:00
2026-07-25 12:36:15 +02:00
2026-07-22 23:49:44 +02:00
2026-07-21 00:00:24 +02:00
2026-07-21 00:42:56 +02:00
2026-07-21 00:10:58 +02:00
2026-07-22 23:49:44 +02:00
2026-07-25 12:33:53 +02:00
2026-07-19 12:21:26 +02:00
2026-07-25 12:33:53 +02:00
2026-07-19 12:21:26 +02:00
2026-07-22 23:49:44 +02:00
2026-07-25 12:33:53 +02:00
2026-07-21 00:37:11 +02:00
2026-07-22 23:10:23 +02:00
2026-07-25 12:33:53 +02:00
2026-07-22 23:10:23 +02:00

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:

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:

    docker login git.krtss.de
    docker compose pull
    
  5. Authenticate the configured OpenCode providers before starting the persistent server:

    docker compose run --rm opencode opencode auth login
    docker compose run --rm opencode opencode auth list
    
  6. Start the services:

    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:

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:

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.

S
Description
A gitea webhook host for agentic development.
Readme
748 KiB
Languages
Python 98.4%
Dockerfile 0.8%
Shell 0.8%