A service for diverting Forgejo webhook notifications to AI agents to do work
Find a file
2026-06-23 11:18:08 -05:00
.gitignore Initial commit 2026-06-23 15:15:53 +00:00
LICENSE Initial commit 2026-06-23 15:15:53 +00:00
README.md Update plan based on feedback 2026-06-23 11:18:08 -05:00

forgejo-agent

A service that receives Forgejo webhook events and dispatches them to AI agents configured to perform automated actions — triage issues, reproduce bugs, review PRs, and more.

Overview

                         ┌──────────────┐
                         │   Forgejo    │
                         │  (self-host) │
                         └──────┬───────┘
                                │ webhook (HTTPS)
                                ▼
                         ┌──────────────┐
                         │    Caddy     │
                         │ (TLS term)   │
                         └──────┬───────┘
                                │ HTTP :8080
                                ▼
                    ┌────────────────────────┐
                    │    forgejo-agent       │
                    │                        │
                    │  ┌──────────────────┐  │
                    │  │ Webhook Receiver │  │
                    │  └────────┬─────────┘  │
                    │           │            │
                    │  ┌────────▼─────────┐  │
                    │  │   Rule Engine    │  │
                    │  └────────┬─────────┘  │
                    │           │            │
                    │  ┌────────▼─────────┐  │
                    │  │  Agent Executor  │──┼──▶ LLM Provider
                    │  │  (langchaingo)   │  │    (OpenAI/Anthropic/...)
                    │  └────────┬─────────┘  │
                    │           │            │
                    │  ┌────────▼─────────┐  │
                    │  │   Tool Runner    │  │
                    │  │                  │──┼──▶ Forgejo API
                    │  │  ┌────────────┐  │  │
                    │  │  │  boxlite   │──┼──▶ Sandbox (run_command)
                    │  │  └────────────┘  │  │
                    │  └──────────────────┘  │
                    │                        │
                    │  ┌──────────────────┐  │
                    │  │    Web UI        │  │
                    │  │  (basic auth)    │  │
                    │  └──────────────────┘  │
                    │                        │
                    │  ┌──────────────────┐  │
                    │  │    Postgres      │  │
                    │  └──────────────────┘  │
                    └────────────────────────┘

Architecture

1. Webhook Receiver (POST /api/webhook)

  • Accepts Forgejo webhook payloads (JSON, content-type application/json).
  • Validates the webhook secret (HMAC-SHA256) against a global secret configured in the credentials store.
  • Persists the raw webhook to the webhooks table.
  • Hands off to the Rule Engine asynchronously (goroutine + worker pool).
  • Returns 200 OK immediately to Forgejo to avoid timeouts.

2. Rule Engine

  • A rule is a user-configured mapping of (repo_pattern, event_type)agent_config.
  • repo_pattern supports globs (e.g. owner/*) to match multiple repositories.
  • Built-in event types mirror the Forgejo webhook events: issues, issue_comment, pull_request, pull_request_comment, push, create, delete, release.
  • Each rule carries:
    • The system prompt / persona for the LLM.
    • Which tools the agent has access to.
    • An explicit allow-list of actions the agent is permitted to take (e.g. "may post comments", "may add labels").
    • Token rate limit (tokens per hour, default 1M).
    • Enabled/disabled toggle.
  • Rules are evaluated in order by priority; the first matching rule wins.
  • A global token rate limit is enforced across all agents (configurable via env var).

3. Agent Executor (langchaingo)

  • Uses langchaingo for LLM interaction, tool calling, and the agent loop.
  • The executor constructs a conversation context from the webhook payload and the rule's system prompt.
  • langchaingo's agent framework drives the LLM interaction, handling the function-calling / tool-use loop automatically.
  • All tool invocations are logged to the actions table along with inputs and results.
  • Support for any provider langchaingo supports: OpenAI, Anthropic, Ollama, Google AI, AWS Bedrock, Azure OpenAI, and others.

4. Tools (what agents can do)

Available tools are registered per rule. Implemented as Go functions using langchaingo's tool.Tool interface:

Tool Description
post_issue_comment Add a comment on an issue or pull request
edit_issue Change title, body, or assignee of an issue
add_labels Add labels to an issue or pull request
close_issue Close an issue or pull request
fetch_issue Get full details of an issue
fetch_diff Retrieve the diff for a pull request
fetch_file Read a file at a given path/ref from the repository
search_code Search repository code via Forgejo's API
get_commit Retrieve details of a specific commit
run_command* Execute a shell command inside a boxlite sandbox (opt-in)

* run_command is isolated via boxlite's Go SDK. Each command invocation creates a lightweight VM sandbox running an OCI container with the configured image (e.g. python:slim, ubuntu:latest). The sandbox is ephemeral — state is not preserved between calls. CPU/memory limits and a timeout are enforced. Must be explicitly enabled per rule.

5. Database Schema (Postgres)

Schema migrations are managed by goose. SQL queries are built using jet, which generates type-safe Go code from a live database. The generated code lives under internal/db/gen/ and is committed to the repository.

-- Incoming webhook log
CREATE TABLE webhooks (
    id          BIGSERIAL PRIMARY KEY,
    received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    event_type  TEXT NOT NULL,        -- e.g. 'issues', 'pull_request'
    repo_path   TEXT NOT NULL,        -- e.g. 'owner/repo'
    payload     JSONB NOT NULL,       -- raw webhook body
    signature   TEXT,                 -- HMAC signature received
    matched_rule_id BIGINT REFERENCES rules(id),
    status      TEXT NOT NULL DEFAULT 'pending'  -- pending | processing | completed | failed
);

-- Action taken by an agent
CREATE TABLE actions (
    id          BIGSERIAL PRIMARY KEY,
    webhook_id  BIGINT NOT NULL REFERENCES webhooks(id),
    rule_id     BIGINT REFERENCES rules(id),
    tool_name   TEXT NOT NULL,        -- which tool was invoked
    tool_input  JSONB,                -- arguments the LLM passed
    tool_output JSONB,                -- result from the tool
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Agent behavior rules
CREATE TABLE rules (
    id             BIGSERIAL PRIMARY KEY,
    name           TEXT NOT NULL,
    repo_pattern   TEXT NOT NULL,      -- glob, e.g. 'owner/*'
    event_type     TEXT NOT NULL,      -- Forgejo event type
    enabled        BOOLEAN NOT NULL DEFAULT true,
    llm_provider   TEXT NOT NULL,      -- e.g. 'openai', 'anthropic', 'ollama'
    llm_model      TEXT NOT NULL,
    system_prompt  TEXT NOT NULL,      -- agent persona / instructions
    allowed_tools  TEXT[] NOT NULL,    -- e.g. '{post_issue_comment, add_labels}'
    max_tokens_per_hour BIGINT NOT NULL DEFAULT 1000000,  -- per-agent rate limit
    priority       INT NOT NULL DEFAULT 0,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at     TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Encrypted credentials (Forgejo API tokens, LLM API keys, etc.)
CREATE TABLE credentials (
    id         BIGSERIAL PRIMARY KEY,
    key        TEXT NOT NULL UNIQUE,   -- e.g. 'forgejo_api_token', 'openai_api_key'
    value      TEXT NOT NULL,          -- AES-256-GCM encrypted value
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Secrets encryption at rest: Credential values are encrypted with AES-256-GCM. The encryption key is derived from the ENCRYPTION_KEY environment variable (a 256-bit hex string). If unset, the process exits with an error.

6. Authentication & Web UI

Web UI authentication is entirely in-memory. Credentials are never written to the database.

  • Basic auth — the browser sends an Authorization header on every request. The server checks it against in-memory credentials. No sessions, no cookies.
  • Admin credentials are set via environment variables:
    • AUTH_USERNAME — admin username.
    • AUTH_PASSWORD — admin password.
  • If neither is set, forgejo-agent generates a random username + password on startup and prints them to stdout. These are held in memory for the lifetime of the process only.
  • The auto-generated credentials are printed at INFO level so they appear in both console and structured log output.

Web UI pages (server-rendered with Go html/template):

Page Description
Dashboard Recent webhook events, action counts, agent status
Rules CRUD list of rules with ordering, enable/disable toggle
Event Log Searchable, filterable, paginated table of webhooks. Click to expand payload, matched rule, actions taken
Credentials Set or update Forgejo API token, LLM API keys, webhook secret

7. Rate Limiting

  • Per-agent limit: Each rule has a max_tokens_per_hour field (default 1,000,000). When an agent hits its limit, the webhook is logged as failed with a rate-limit reason. Limits reset on a rolling window.
  • Global limit: GLOBAL_MAX_TOKENS_PER_HOUR env var (default 10,000,000) caps total token consumption across all agents. When hit, new webhooks are queued until the window rolls forward or rejected if the queue is full.
  • Token counting uses the LLM provider's reported usage from each API response for accurate metering.

8. Deployment

┌──────────────────────────────────────────────────────┐
│  Host / VM                                            │
│                                                       │
│  caddy (TLS termination, reverse proxy to :8080)      │
│  forgejo-agent (single Go binary, listens :8080)      │
│  postgres (or external connection string)              │
└──────────────────────────────────────────────────────┘
  • Caddyfile example:
agent.example.com {
    reverse_proxy localhost:8080
}
  • forgejo-agent is configured via environment variables:
Env var Description Default
DATABASE_URL Postgres connection string postgres://localhost:5432/forgejo_agent
LISTEN_ADDR HTTP listen address :8080
BASE_URL Public base URL (used in generated links) http://localhost:8080
LOG_LEVEL zerolog level (debug, info, warn, error, fatal) info
LOG_FORMAT Log format (json, console) json (auto-detects TTY)
AUTH_USERNAME Admin username for basic auth auto-generated if unset
AUTH_PASSWORD Admin password for basic auth auto-generated if unset
ENCRYPTION_KEY 256-bit hex key for credential encryption at rest (required)
MAX_CONCURRENT_AGENTS Agent worker pool size 4
GLOBAL_MAX_TOKENS_PER_HOUR Global token rate limit 10000000
WEBHOOK_QUEUE_SIZE Pending webhook buffer size 100

Development Workflow

Schema changes

  1. Write a new SQL migration file in migrations/ using goose conventions (e.g. 00002_add_rate_limits.sql).
  2. Apply with goose -dir migrations postgres "$DATABASE_URL" up.
  3. Regenerate jet types: jet -dsn="$DATABASE_URL" -schema=public -path=./internal/db/gen.
  4. Commit both the migration file and the regenerated Go code.

The generated code under internal/db/gen/ must always be in sync with the current migration state. CI should verify this (run jet and check for a clean git diff).

Implementation Plan

Phase 1 — Skeleton

  • Project layout following standard Go conventions: cmd/, internal/.
  • CLI entry point using cobra. Root command starts the server; future subcommands can handle credential rotation, health checks, etc.
  • zerolog for structured logging:
    • JSON output to stderr by default.
    • zerolog.ConsoleWriter with colorized output when LOG_FORMAT=console or when running on an interactive terminal (isatty auto-detection).
  • Auto-run goose migrations on startup.
  • Basic HTTP router using net/http + chi.
  • Caddy in front for TLS during development.

Phase 2 — Webhook Ingestion

  • Webhook handler: verify HMAC signature, parse JSON, insert into webhooks table using jet-generated query builders.
  • Rule matching engine: load rules from DB (using jet), match repo_path glob
    • event_type, resolve first match by priority.
  • Worker pool: N goroutines pull from a buffered channel, process webhooks.

Phase 3 — Agent Loop with langchaingo

  • Integrate langchaingo's agent package. Configure LLM providers via langchaingo's built-in backends (OpenAI, Anthropic, Ollama, etc.).
  • Define tools as langchaingo tool.Tool implementations.
  • Agent loop: langchaingo manages the conversation, tool calling, and response handling internally.

Phase 4 — Tools

  • Implement Forgejo API tools (post_issue_comment, edit_issue, add_labels, close_issue, fetch_issue, fetch_diff, fetch_file, search_code, get_commit). Each tool reads the Forgejo API token from the credentials table (decrypted at load time).
  • Implement run_command using the boxlite Go SDK:
    • Create ephemeral boxlite Box from a configurable OCI image.
    • Execute the command inside with a timeout and resource limits.
    • Capture stdout/stderr and return them to the LLM.
    • Destroy the box after execution.

Phase 5 — Web UI

  • Server-rendered HTML using Go html/template. Minimal CSS.
  • HTTP basic auth middleware checks credentials on every request against in-memory values (set via AUTH_USERNAME/AUTH_PASSWORD env vars or auto-generated at startup).
  • Pages: Dashboard, Rules CRUD, Event Log (paginated), Credentials management.
  • Auto-generate admin credentials on startup if not provided, log them.

Phase 6 — Rate Limiting & Operations

  • Token-based rate limiting: per-rule and global, using a rolling-window counter keyed by rule ID (in-memory, backed by atomic counters).
  • Prometheus metrics endpoint (/metrics).
  • Graceful shutdown with webhook drain.
  • Health check endpoint (/health).
  • CLI subcommand to rotate the encryption key.

Project Layout

forgejo-agent/
├── cmd/
│   └── forgejo-agent/
│       └── main.go               # cobra root command, server startup
├── internal/
│   ├── webhook/                  # webhook handler, HMAC validation
│   ├── rules/                    # rule engine, matching logic
│   ├── agent/                    # agent executor (langchaingo integration)
│   │   └── tools/                # tool implementations (Forgejo API + boxlite)
│   ├── db/
│   │   ├── gen/                  # jet-generated types and query builders (committed)
│   │   └── migrations.go        # goose migration runner
│   ├── auth/                     # basic auth middleware
│   ├── ui/                       # HTML templates, handlers
│   │   └── templates/
│   ├── ratelimit/                # token rate limiter
│   └── config/                   # configuration loading
├── migrations/                   # goose SQL migration files
│   └── 00001_initial.sql
├── scripts/                      # helper scripts (dev caddy, etc.)
├── Dockerfile
├── go.mod
├── go.sum
└── README.md

Dependencies

Library Purpose
github.com/spf13/cobra CLI framework
github.com/go-chi/chi/v5 HTTP router and middleware
github.com/rs/zerolog Structured logging with colorized console output
github.com/jackc/pgx/v5 Postgres driver
github.com/pressly/goose/v3 Database migration engine
github.com/go-jet/jet/v2 Type-safe SQL query builder (code generation from DB)
github.com/tmc/langchaingo LLM integration, agent framework, tool calling
github.com/boxlite-ai/boxlite/sdks/go Sandboxed command execution for agents
github.com/gobwas/glob Glob matching for repo patterns
golang.org/x/crypto bcrypt (not needed for auth; AES-256-GCM for credential encryption)
golang.org/x/term Terminal detection for zerolog console writer auto-enable

Open Questions

  1. Boxlite image management — The run_command tool needs an OCI image to boot. Should each rule configure its own image (e.g. python:slim for issue reproduction, ubuntu:latest for build checks), or use a single global default? A per-rule default with a global fallback seems most flexible.

  2. Repository access for run_command — To reproduce bugs, the sandbox needs a clone of the repository. Options: (a) shallow-clone on every run_command call inside the boxlite VM, (b) use a read-only volume mount of a pre-cloned repo. Option (a) is simpler; option (b) is faster for repos with large histories. Start with (a) and add (b) if needed.

  3. Webhook retry / durability — If forgejo-agent is down when a webhook is sent, Forgejo will retry according to its own retry policy. Is that sufficient, or do we need an out-of-process queue (e.g. Redis)? The in-process channel + Postgres write-before-process should be sufficient for single-node deployments.