This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.

CI Agent Runner

This repository contains a stateful, webhook-driven Gitea agent server. It plans issues, reviews plans, implements accepted plans, independently reviews diffs, and publishes pull requests without using Gitea Actions or a Gitea runner.

Architecture

The deployment has two privilege-separated services:

  • controller receives signed webhooks and owns the Gitea write token. It claims requests, publishes comments and labels, validates results, pushes branches, and creates pull requests. It does not contain the OpenCode executable or OAuth data.
  • executor owns the Gitea read token and OpenCode OAuth data. It checks out trusted revisions and runs agents. It never receives the Gitea write token or webhook secret.

Both services share a local SQLite database and disposable workspaces. SQLite runs in WAL mode, so the shared state directory must be on a local filesystem rather than NFS or another network filesystem. Execution concurrency is one because OpenAI OAuth refresh tokens can rotate.

OpenCode conversations are durable:

  • One planner conversation is retained per issue.
  • One implementer conversation is retained per issue and accepted-plan digest.
  • Every review iteration uses a fresh reviewer session.
  • Current Gitea state, repository contents, and stored digests remain authoritative over conversation memory.

Triggers

Existing labels remain supported:

  • agent:plan
  • agent:implement
  • agent:fix-review on an agent-generated pull request

The controller consumes a trigger label after durably admitting its request. Gitea 1.26 label webhooks expose the resulting label set rather than the exact label that changed, so the controller reconciles current issue state and tracks each claim in SQLite.

New issue comments can use these commands:

/agent plan [optional instruction]
/agent implement [optional instruction]
/agent discuss [question, comment, or adjustment]
/agent review [optional focus]
/agent fix [optional instruction]
/agent continue [optional instruction]
/agent retry [optional instruction]
/agent cancel
/agent status

Commands must begin the comment and are processed only on comment creation. /agent discuss is accepted on issues with a current plan. /agent review reviews the current plan when posted on an issue and any open pull request when posted there. /agent fix and agent:fix-review are accepted only on pull requests previously published by this service. Command edits, bot command comments, unauthorized users, and non-command webhook deliveries are ignored. Ordinary human issue comments remain part of the issue digest and invalidate stale accepted plans. Agent command comments are control messages and are excluded from that digest.

Plan discussions resume the issue's planner conversation and always post a new issue response. Questions leave the current plan unchanged; requested adjustments immediately replace the implementable plan without invoking the independent reviewer. Run /agent review separately for a fresh, one-pass plan review.

Implementation fixes resume the accepted-plan implementer conversation, consume submitted human review summaries and inline comments plus marked agent reviews, update the generated branch when needed, and post a pull-request response. Feedback edits are processed as new versions. Fix turns do not automatically re-review their changes; run /agent review again when wanted. Human pending reviews must be submitted before the bot can read them.

Security Model

  • Webhooks require X-Gitea-Signature, verified as HMAC-SHA256 over the exact request bytes.
  • The webhook body is limited to 1 MiB and must be JSON.
  • The configured repository ID and full name must match every payload.
  • Actor authorization is fail-closed. Numeric Gitea user IDs are preferred over logins.
  • Webhook deliveries and logical requests are independently deduplicated.
  • OpenCode receives an explicitly limited tool policy and only the read token.
  • External Exa and grep.app MCPs are available only to a sandboxed research subagent with no local file or Gitea access; callers may delegate only generic public library and best-practice questions.
  • Repository code is never executed by the credential-bearing services.
  • Git hooks, signing, global configuration, text conversion, symlinks, unsafe paths, and protected automation paths are blocked or independently validated.
  • The publisher revalidates the issue digest, accepted plan, default branch, Git metadata, changed files, file types, and remote branch state before writing.

Prerequisites

  • Gitea 1.26 or newer.
  • Docker Engine with Compose v2, or compatible Podman tooling.
  • A dedicated bot account with a repository-scoped write token.
  • A separate read-only Gitea token for the executor and Gitea MCP.
  • An OpenAI account usable by OpenCode.
  • A reverse proxy providing HTTPS to the controller.

The images pin OpenCode CLI 1.18.2, OpenCode SDK 1.17.18, and Gitea MCP 1.3.0.

Configuration

Create the environment file and state directories:

cp deploy/.env.example deploy/.env
sudo install -d -o 10001 -g 10001 -m 0700 \
  /srv/gitea-agent/state \
  /srv/gitea-agent/workspaces \
  /srv/gitea-agent/opencode \
  /srv/gitea-agent/cache

Set GITEA_SERVER_URL, GITEA_REPOSITORY, and CI_AGENT_BOT_LOGIN in .env. Configure at least one actor allowlist:

CI_AGENT_ALLOWED_ACTOR_IDS=10,11
CI_AGENT_ALLOWED_ACTORS=

Create local secret files:

install -d -m 0700 deploy/secrets
install -m 0600 /dev/null deploy/secrets/gitea-write-token
install -m 0600 /dev/null deploy/secrets/gitea-read-token
install -m 0600 /dev/null deploy/secrets/gitea-webhook-secret

Populate them as follows:

  • gitea-write-token: bot PAT able to read issues and write comments, labels, branches, and pull requests.
  • gitea-read-token: separate PAT restricted to repository, issue, comment, and pull-request reads.
  • gitea-webhook-secret: a randomly generated webhook secret, for example openssl rand -hex 32.

Local Docker and Podman Compose implementations bind-mount file-backed secrets and may ignore the Compose uid, gid, and mode fields. After populating the files, make them readable only by the container identity:

sudo chown 10001:10001 deploy/secrets/gitea-write-token deploy/secrets/gitea-read-token deploy/secrets/gitea-webhook-secret
sudo chmod 0400 deploy/secrets/gitea-write-token deploy/secrets/gitea-read-token deploy/secrets/gitea-webhook-secret

Do not place tokens directly in .env.

OpenCode Authentication

Build the executor and run the interactive login with the persistent OAuth volume:

docker compose --env-file deploy/.env -f deploy/compose.yaml build executor
docker compose --env-file deploy/.env -f deploy/compose.yaml run --rm --no-deps --entrypoint opencode executor \
  auth login --provider openai --method "ChatGPT Pro/Plus (headless)"

Verify authentication:

docker compose --env-file deploy/.env -f deploy/compose.yaml run --rm --no-deps --entrypoint opencode executor auth list
docker compose --env-file deploy/.env -f deploy/compose.yaml run --rm --no-deps --entrypoint opencode executor models openai
docker compose --env-file deploy/.env -f deploy/compose.yaml run --rm --no-deps --entrypoint opencode executor \
  run --model openai/gpt-5.6-sol "Reply with exactly: OPENCODE_AGENT_AUTH_OK"

OAuth credentials and OpenCode's conversation database remain under ${OPENCODE_DATA_DIR} and are never mounted into the controller.

Gitea Webhook

Create a repository webhook with:

  • Target URL: https://agent.example.com/webhooks/gitea
  • Content type: application/json
  • Secret: the exact content of gitea-webhook-secret
  • Events: issue label changes, issue comments, pull request comments, and pull request labels
  • Active: enabled

Gitea's test-delivery button sends a push event rather than an issue event. The server accepts only the configured issue event types, so validate the installation by creating a test issue and adding agent:plan or posting /agent status.

The webhook endpoint commits each verified delivery before returning 204. Gitea 1.26 does not automatically retry failed HTTP deliveries, but manual replay is safe because delivery and business keys are deduplicated separately.

Start

docker compose --env-file deploy/.env -f deploy/compose.yaml build --pull
docker compose --env-file deploy/.env -f deploy/compose.yaml up -d
docker compose --env-file deploy/.env -f deploy/compose.yaml logs -f controller executor

Health endpoints:

GET /healthz
GET /readyz

By default Compose binds the controller to 127.0.0.1:8080; expose it through a reverse proxy. Do not expose the executor.

Operation

Initial planning creates or resumes the issue's planner conversation. An independent reviewer can request up to three revisions. Later /agent discuss turns are conversational and do not review automatically. Accepted and directly adjusted plans are published with the protocol-v1 gitea-agent marker namespace.

Plans published under an earlier marker namespace are intentionally not imported after this product rename. Replan outstanding issues once before requesting implementation.

Keep CI_AGENT_BOT_LOGIN set to the account that authored existing protocol-v1 plan comments if those plans must remain implementable. Changing bot accounts requires replanning outstanding issues.

Initial implementation requires an accepted plan whose issue digest and base SHA are current. The executor uses a deterministic branch named agent/issue-<number>-p<digest>. The controller publishes only after independently validating the reviewed workspace and remote branch state. If blocking findings remain after the final implementation review iteration, a non-empty implementation is still published as a pull request and the remaining findings are posted as a marked Gitea comment review. A rejected implementation with no diff remains a failed request because no pull request can be created. Execution, safety, and stale-state failures are never published. /agent review performs one fresh review of any open pull request and publishes a Gitea comment review with validated inline anchors. Only pull requests tied to a durable published implementation record can use /agent fix or agent:fix-review; marked reviews published after review exhaustion are available to the next fix attempt.

/agent cancel sets a durable cancellation flag. The executor checks it while heartbeating and propagates cancellation into Gitea requests, Git subprocesses, and OpenCode prompts. Interrupted running jobs return to the queue on service restart; interrupted publications remain in the outbox and are retried.

Backup And Recovery

Back up both application state and OpenCode data. For a simple consistent offline backup:

docker compose --env-file deploy/.env -f deploy/compose.yaml stop
sudo cp -a /srv/gitea-agent/state /backup/gitea-agent-state
sudo cp -a /srv/gitea-agent/opencode /backup/gitea-agent-opencode
docker compose --env-file deploy/.env -f deploy/compose.yaml start

Workspaces are disposable and do not need backup. If an OpenCode session is unavailable after restore, the executor creates a replacement conversation while retaining durable job and Gitea artifacts.

Development

Biome provides formatting, import organization, and static analysis. It enforces four-space indentation; lint:fix applies safe fixes, while lint:fix:unsafe is available only for explicitly reviewed semantic fixes. The check command also enforces the repository limits of three files and four folders per directory and 250 lines per non-test source file.

npm --prefix app ci
npm --prefix app run lint:fix
npm --prefix app run check
npm --prefix app test
docker compose --env-file deploy/.env -f deploy/compose.yaml config
docker compose --env-file deploy/.env -f deploy/compose.yaml build

Tests cover webhook signatures and command parsing, canonical issue snapshots, delivery deduplication, label claims, job leases, outbox transitions, and restart-safe SQLite state.

Cutover From The Runner

  1. Build and deploy the controller in a non-executing environment and verify health.
  2. Disable the old Gitea Actions issue workflow and stop the registered agent runner.
  3. Configure the repository webhook.
  4. Start the controller and executor.
  5. Trigger one planning request and verify its status, accepted-plan marker, and durable conversation.
  6. Trigger implementation and verify branch and pull-request reuse.
  7. Remove the old runner registration after the rollback window.

Do not run the old workflow and this server against the same trigger labels simultaneously.

S
Description
a web hook base gitea agentic runner
Readme
334 KiB
Languages
TypeScript 95.9%
JavaScript 3.2%
Dockerfile 0.8%