No description
  • Go 54.3%
  • TypeScript 41.8%
  • Shell 1.3%
  • CSS 1.2%
  • Makefile 0.8%
  • Other 0.6%
Find a file
Robin Gasi e9d9d4f123 feat: pre-flight setup screen, box credential flags, and product PRD
- Daemons report ClaudeAuth/GitAuth capability flags in the Hello frame;
  stored on instances and exposed via the API.
- The board shows a live pre-flight checklist until the fleet can run a
  task (box enrolled, Claude authorized, git credential, repo added) —
  checks verify themselves as daemons report in. Replaces the static
  onboarding empty-state.
- Fleet cards gain per-box CLAUDE/GIT credential chips.
- docs/PRD.md: scope contract from the consolidation session — wedge,
  locked decisions, v0.1 "phone-at-breakfast" / v0.2 "fleet brain"
  milestones, parked items with re-entry triggers.
2026-07-09 22:53:56 +02:00
.claude/skills/serval feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
.github feat: initial caracal-web control plane (serval + TanStack Start) 2026-07-08 19:09:31 +02:00
cmd feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
config feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
db feat: pre-flight setup screen, box credential flags, and product PRD 2026-07-09 22:53:56 +02:00
docker feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
docs feat: pre-flight setup screen, box credential flags, and product PRD 2026-07-09 22:53:56 +02:00
frontend feat: pre-flight setup screen, box credential flags, and product PRD 2026-07-09 22:53:56 +02:00
internal feat: pre-flight setup screen, box credential flags, and product PRD 2026-07-09 22:53:56 +02:00
routes feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
schedule feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
.env.example feat: initial caracal-web control plane (serval + TanStack Start) 2026-07-08 19:09:31 +02:00
.gitignore feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
AGENTS.md feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
assets.go feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
caracal-agentd feat: initial caracal-web control plane (serval + TanStack Start) 2026-07-08 19:09:31 +02:00
CLAUDE.md feat: initial caracal-web control plane (serval + TanStack Start) 2026-07-08 19:09:31 +02:00
DEPLOY.md feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
Dockerfile feat: add new feature 2026-07-09 17:02:10 +02:00
go.mod feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
go.sum feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
install-agent.sh feat: add new feature 2026-07-09 17:02:10 +02:00
main.go feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
Makefile feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
openapi.json feat: pre-flight setup screen, box credential flags, and product PRD 2026-07-09 22:53:56 +02:00
README.md feat: close the agent loop — ship, review, iterate — and go self-contained 2026-07-09 22:09:15 +02:00
serval.yaml feat: initial caracal-web control plane (serval + TanStack Start) 2026-07-08 19:09:31 +02:00
sqlc.yaml feat: initial caracal-web control plane (serval + TanStack Start) 2026-07-08 19:09:31 +02:00

caracal-web

Self-hosted cloud coding agents on your own hardware and your own Claude subscription. Think Cursor background agents / Codex, minus the vendor: you enroll any Ubuntu box, dispatch tasks from a web dashboard, watch the agent work live, review the diff, and open a pull request on your own Gitea — all without anything leaving your network except the model API calls themselves.

Each enrolled box runs a small Go daemon that executes Claude Code in throwaway, hardened Docker sandboxes and streams the transcript back over a single dial-out WebSocket.

The loop

dispatch → work → review → iterate → ship

  • Dispatch — pick a repo, write a prompt, choose a model. The task lands on the box's daemon (live, or queued in an outbox until it reconnects).
  • Work — the daemon shallow-clones the repo onto an agent branch (caracal/agent-<id>), runs Claude Code in a sandbox (--memory 4g --cpus 2 --cap-drop ALL, non-root), and streams every thinking/tool/result step to the dashboard in real time.
  • Review — when the run changes files, the daemon commits, diffs against the base branch, and pushes the agent branch. The diff renders in the run view (per-file, collapsible, +/ stats); the task lands in review.
  • Iterate — send a follow-up from the run page. The agent resumes its Claude session (state persists across sandboxes) on a fresh clone of its own pushed branch, so prior work carries over. Costs accumulate per task.
  • Ship — one click opens a pull request on your Gitea via its API, with a backlink to the run.

Runs that change nothing simply finish as done — no branch, no noise.

How it fits together

Browser ──HTTP + SSE──►  control plane (this repo)             your box
                          Go API + WS gateway   ◄──WS──  caracal-agentd
                          Postgres (data + LISTEN/NOTIFY)      │ docker run
                          Gitea API (PRs)                      ▼
                                                        claude -p (sandbox)
                                                         │ git push
                                                         ▼
                                                        your Gitea
  • The daemon dials OUT (NAT-friendly, zero inbound ports) and authenticates with a per-instance token minted at enrollment.
  • Transcript frames flow daemon → gateway → Postgres (pg_notify) → the browser's SSE stream. Nothing is polled that can be pushed.
  • URLs are self-configuring: install commands, the bootstrap script, and the daemon's WebSocket URL are derived from the address each client actually used (LAN IP, localhost, tailnet, or domain — reverse-proxy aware). PUBLIC_URL exists only as a fallback.

Quickstart (local dev)

Requires go, bun, docker. The app is fully self-contained — the web framework is vendored in internal/serval/; there is no CLI to install.

cp .env.example .env   # fill APP_KEY (make key) and DATABASE_URL
make setup             # bun install + start Postgres + migrate
make dev               # API :3000 + frontend :5173 + worker + scheduler, hot reload

Open http://localhost:5173, register an account, and you're on the board. make help lists every task.

Add an agent box

Dashboard → Fleet → Add machine mints a one-time enrollment token and prints a paste-ready one-liner (the URL is whatever host your browser used, so it works on a LAN out of the box):

curl -fsSL http://192.168.1.20:3000/i/<enrollment-token> | sh
#                                             …add: | sh -s -- --harden

The bootstrap downloads the prebuilt caracal-agentd (checksum-verified, or builds from source), installs a systemd service, builds the sandbox image, and prompts for credentials. --harden firewalls the box and enables unattended security upgrades. Details: DEPLOY.md.

Credentials — who holds what

Credential Lives Used for
CLAUDE_CODE_OAUTH_TOKEN (claude setup-token, uses your Pro/Max plan) or ANTHROPIC_API_KEY box daemon env (/etc/caracal-agent/env, 0600) forwarded into each sandbox for Claude Code
CARACAL_GIT_TOKEN (+ optional CARACAL_GIT_USERNAME) box daemon env clone private repos, push agent branches — injected via an inline git credential helper, never in argv, never in the DB, never inside the sandbox
GITEA_URL / GITEA_TOKEN server env (.env) the "Open PR" action (Gitea REST API)

The sandbox container only ever sees the Claude credential; git operations happen host-side on the box.

Self-host

One image, role-based commands (serve / queue:work / schedule:run / migrate):

cd docker
cp ../.env.example .env        # fill APP_KEY (make key), optionally GITEA_*
docker compose -f compose.selfhost.yml up -d --build

For a LAN-only deployment nothing else is needed — access the dashboard by the server's LAN IP and every generated URL follows. The only egress the boxes need is the Anthropic API and your git host.

Layout

  • Backend (Go): handlers in internal/handlers, DI in internal/providers, routes in routes/. Daemon↔gateway protocol: internal/wire. WS hub + SSE fan-out: internal/gateway. Gitea client: internal/gitea. Request-derived URLs: internal/requrl. SQL in db/queries/*.sql (make sqlc), migrations in db/migrations.
  • Framework: vendored at internal/serval/ (chi router with typed handlers, sqlc/pgx, samber/do DI, River queues, goose migrations, argon2id auth + sessions). It's part of this codebase — edit it deliberately.
  • Daemon (Go): cmd/caracal-agentd + internal/agent — dial-out WS client, sandbox lifecycle (sandbox.go), git finalize/push (ship.go), installer subcommands. Embeds the sandbox Dockerfile.
  • Frontend (TanStack Start + Tailwind): frontend/ (bun). Hand-written typed client in src/lib/api.ts, live transcript via EventSource, diff viewer in src/components/diff-view.tsx.
  • Dev supervisor: cmd/dev (make dev) — rebuild on save, sqlc on query change, vite alongside.

Verify

make check     # go build + vet + frontend typecheck (the CI gate)
make test      # Go tests, including the vendored framework's
go run . routes