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.
Files
ci-agent-runner/README.md
T
2026-07-13 18:50:38 +02:00

212 lines
10 KiB
Markdown

# Olixero Agent Server
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`
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:
```text
/agent plan [optional instruction]
/agent implement [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. Pull-request comments, command edits, bot comments, unauthorized users, and non-command comments are ignored. Ordinary human comments remain part of the issue digest and invalidate stale accepted plans. Agent command comments are control messages and are excluded from that digest.
## 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 disabled by default to avoid private-repository data leakage.
- 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 and SDK 1.17.18 and Gitea MCP 1.3.0.
## Configuration
Create the environment file and state directories:
```bash
cp deploy/.env.example deploy/.env
sudo install -d -o 10001 -g 10001 -m 0700 \
/srv/olixero-agent/state \
/srv/olixero-agent/workspaces \
/srv/olixero-agent/opencode \
/srv/olixero-agent/cache
```
Set `GITEA_SERVER_URL`, `GITEA_REPOSITORY`, and `CI_AGENT_BOT_LOGIN` in `.env`. Configure at least one actor allowlist:
```dotenv
CI_AGENT_ALLOWED_ACTOR_IDS=10,11
CI_AGENT_ALLOWED_ACTORS=
```
Create local secret files:
```bash
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:
```bash
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:
```bash
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:
```bash
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 and issue comments
- 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
```bash
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:
```text
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
Planning creates or resumes the issue's planner conversation. An independent reviewer can request up to three revisions. The accepted plan is published in a protocol-v1 marked comment so plans created by the previous runner remain discoverable.
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.
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.
`/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:
```bash
docker compose --env-file deploy/.env -f deploy/compose.yaml stop
sudo cp -a /srv/olixero-agent/state /backup/olixero-agent-state
sudo cp -a /srv/olixero-agent/opencode /backup/olixero-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.
```bash
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.