Files
handler/docker-compose.yml
T
Claude 71a7550f48 feat: git servers own credentials, one-line project adds with auto-clone, and scheduled agents
Git servers (forge_hosts) become full credential owners:
- an encrypted forge token (Fernet, HANDLER_SECRET_KEY) stored per server and
  never returned by the API (has_token flag only); used automatically by every
  project on that host and addressable as db:host:<hostname> — the reserved
  db: credential scheme is now live
- a per-server ed25519 SSH deploy key: generated server-side, public half
  shown in the dashboard to paste into the forge, private half encrypted at
  rest and materialized 0600 only in the control container (GIT_SSH_COMMAND /
  core.sshCommand)

Project registration gets a git-server mode: pick a registered server, type
owner/name, and the API derives the remote (ssh when the server has a deploy
key, https otherwise), computes root_dir under PROJECTS_ROOT, and enqueues a
new 'sync' command the worker executes (clone, or ff-only pull). Spawn always
pulls first, so runs start from the remote's latest state; POST /projects/:p/sync
and 'handler sync' re-pull on demand.

Schedules: recurring agent spawns (prefix, prompt, interval, role). The worker
fires due schedules as ordinary queued spawn commands with timestamped agent
names, so runs are fresh stateless agents and appear in the Activity audit
trail; missed intervals collapse into one catch-up run.

Dashboard: Git Servers pane shows the SSH public key (copy button) and takes a
write-only token; Repositories gains the server-first add form and a Pull now
button; new Schedules pane. Rebuilt static export. Also restores the missing
frontend/lib (api client + format helpers) the components import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XY1tEhQZXHZ5wci7dLc7rM
2026-07-10 19:17:12 +00:00

80 lines
3.0 KiB
YAML

# Local/dev deployment: API + Postgres. For a single-node SQLite deploy, drop the
# `db` service and DATABASE_URL override — the image defaults to SQLite on the
# handler-data volume.
services:
api:
image: ghcr.io/0xwheatyz/handler:latest
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql+psycopg://handler:handler@db:5432/handler
# Required — the API refuses to start without it. Set in .env or the shell.
AUTH_TOKEN: ${AUTH_TOKEN:?set AUTH_TOKEN in .env or the environment}
SHARED_CONTEXT_WRITE_TOKEN: ${SHARED_CONTEXT_WRITE_TOKEN:-}
WEBHOOK_URL: ${WEBHOOK_URL:-}
UI_ENABLED: ${UI_ENABLED:-true}
CORS_ORIGINS: ${CORS_ORIGINS:-}
# The API computes new projects' root_dir under this path (shared volume with the
# control container, which does the actual cloning).
PROJECTS_ROOT: /var/lib/handler/projects
# Encrypts git-server tokens/SSH keys at rest; must match the control container.
HANDLER_SECRET_KEY: ${HANDLER_SECRET_KEY:-}
volumes:
- handler-data:/var/lib/handler
depends_on:
db:
condition: service_healthy
restart: unless-stopped
# Control layer: the `handler` worker. Drains the control-command queue the API enqueues
# (spawn/kill/resume/approve/reject/forge-init/poll-ci/sync), fires due schedules, and
# sweeps CI on an interval.
# Shares the database and the handler-data volume with the API. It waits for the API
# (which owns migrations), so RUN_MIGRATIONS is off here to avoid a startup race. Run
# one-shot control commands against the same image with, e.g.,
# `docker compose run --rm control handler list`. Live agent spawning also needs
# `git`/`tmux` (baked in) plus bring-your-own `claude`/`forge` binaries — layer or mount
# those in.
control:
image: ghcr.io/0xwheatyz/handler/control:latest
build:
context: .
dockerfile: Dockerfile.control
environment:
DATABASE_URL: postgresql+psycopg://handler:handler@db:5432/handler
RUN_MIGRATIONS: "false"
PROJECTS_ROOT: /var/lib/handler/projects
# Per-project forge credentials are resolved from credential_ref pointers at spawn;
# export the referenced vars here when spawning agents from this container.
FORGE_VERSION: ${FORGE_VERSION:-}
# Decrypts git-server tokens/SSH keys stored by the API; must match the API's key.
HANDLER_SECRET_KEY: ${HANDLER_SECRET_KEY:-}
volumes:
- handler-data:/var/lib/handler
depends_on:
db:
condition: service_healthy
api:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: handler
POSTGRES_PASSWORD: handler
POSTGRES_DB: handler
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U handler -d handler"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
volumes:
handler-data:
postgres-data: