Self-hosted, team-oriented AI coding agent platform. Work is organised into Projects — shared workspaces with their own files, members, and one or more Harnesses (agent templates). Each Harness runs inside an isolated Kubernetes Sandbox Pod, and users interact with it through Sessions.
The platform is harness-agnostic. Two transports are supported:
- HTTP harnesses (e.g.
opencode,claude-code) — implement a common HTTP contract; the backend translates their event streams into a uniform protocol the frontend can render. - Terminal harnesses (e.g.
codex,hermes) — wrap a TUI; the backend proxies raw PTY bytes over WebSocket to a terminal view.
graph TD
Browser["Browser\nNext.js + shadcn (port 3000)"]
subgraph Backend["Go Backend (Echo, port 8080)"]
Auth["AuthMiddleware\n(JWT)"]
Router["Router /api/v1"]
SandboxMgr["sandbox.Manager\nEnsureReady / Stop"]
HarnessReg["harness.Registry\nKindHTTP / KindTerminal"]
K8sClient["k8s.Client\nRunTask · EnsurePVC · ResolveURL"]
DB["PostgreSQL\nprojects · harnesses · sessions · users"]
end
subgraph K8s["Kubernetes Cluster"]
SandboxCR["Sandbox CR\nagents.x-k8s.io/v1alpha1\n(one per Harness)\nmounts workspace PVC rw + skills PVC ro"]
subgraph HTTPPods["HTTP Harnesses (SSE + translator)"]
OpenCode["opencode Pod"]
ClaudeCode["claude-code Pod"]
end
subgraph TermPods["Terminal Harnesses (raw PTY over WS)"]
Codex["codex Pod"]
Hermes["hermes Pod"]
end
FileMgr["filemgr Pod\n(one per Project)"]
SkillMgr["skillmgr Pod\n(cluster-wide)"]
WorkPVC["workspace PVC\ncattery-project-<id>-work"]
SkillPVC["skills PVC\ncattery-skills-work"]
end
ModelAPI["External Model API\nAnthropic / OpenAI-compatible gateway"]
Browser -->|"REST / SSE / WebSocket"| Auth
Auth --> Router
Router -->|"session/message"| SandboxMgr
Router -->|"handlers → stores"| DB
Router -->|"proxy /files/*"| FileMgr
Router -->|"proxy /skills/*"| SkillMgr
SandboxMgr --> K8sClient
SandboxMgr -->|"KindFor(harnessID)"| HarnessReg
K8sClient -->|"create/delete Sandbox CR\n(spec includes PVC mounts)"| SandboxCR
K8sClient -->|"create Pod"| FileMgr
K8sClient -->|"create Pod"| SkillMgr
K8sClient -->|"EnsurePVC"| WorkPVC
K8sClient -->|"EnsurePVC"| SkillPVC
SandboxCR --> OpenCode
SandboxCR --> ClaudeCode
SandboxCR --> Codex
SandboxCR --> Hermes
OpenCode -->|"SSE events → translator → PlatformEvent"| Router
ClaudeCode -->|"SSE events → translator → PlatformEvent"| Router
Codex -->|"PTY bytes"| Router
Hermes -->|"PTY bytes"| Router
OpenCode --> ModelAPI
ClaudeCode --> ModelAPI
Codex --> ModelAPI
Hermes --> ModelAPI
FileMgr -->|"mount rw"| WorkPVC
SkillMgr -->|"mount rw"| SkillPVC
- Kubernetes cluster (any flavor — kind, minikube, EKS, GKE, etc.) with kubeconfig reachable from the backend
- Agent Sandbox controller installed in the cluster — provides the
agents.x-k8s.io/v1alpha1SandboxCRD that Cattery uses to launch isolated agent pods - PostgreSQL 14+ — run it yourself, or let the bundled
docker-compose.ymlstart one for you - Go ≥ 1.22 (only if you run the backend on the host instead of in compose)
- Bun ≥ 1.0 (only if you run the frontend on the host)
- Docker — required for harness images, plus the optional compose stack
- An OpenAI- or Anthropic-compatible model gateway (e.g. NewAPI, LiteLLM, the upstream provider directly)
The k8s/ directory bundles everything the cluster needs behind one Kustomize
entrypoint — the upstream Agent Sandbox controller (version-pinned), the
cattery namespace, and the backend's ServiceAccount + RBAC:
kubectl apply -k k8s/This fetches the Agent Sandbox controller manifest over the network; to pin a
different release, edit the URL in k8s/kustomization.yaml
(see the agent-sandbox releases page).
Preview the fully-rendered manifests first with kubectl kustomize k8s/.
Verify the CRD is registered and the controller is running:
kubectl get crd sandboxes.agents.x-k8s.io
kubectl get pods -n agent-sandbox-systemSee the Agent Sandbox Getting Started guide for the controller's own RBAC, namespacing, and extension components.
# 1. clone
git clone https://github.com/wellCh4n/cattery.git && cd cattery
# 2. backend env
cat > backend/.env <<EOF
DATABASE_URL=postgres://postgres:postgres@localhost:5432/cattery?sslmode=disable
PORT=8080
K8S_NAMESPACE=default
ANTHROPIC_BASE_URL=https://api.anthropic.com
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_API_KEY=sk-...
EOF
# 3. build harness images so the cluster can pull them
make build-harness # builds opencode, claude-code, codex, hermes
# tag and push to your registry as needed:
# docker tag opencode-sandbox:dev your-registry/opencode-sandbox:dev
# docker push your-registry/opencode-sandbox:dev
# 4. run — pick one of the modes belowPick the mode that fits your setup:
# A) Everything in Docker (db + backend + web)
docker compose -f docker/docker-compose.yml up -d
# B) Backend + web in Docker, point at an existing external database
DATABASE_URL='postgres://user:pw@host.docker.internal:5432/cattery?sslmode=disable' \
docker compose -f docker/docker-compose.yml up -d --no-deps backend webOpen http://localhost:3000, sign in with the bootstrap admin credentials (see Authentication), then create your first Project from the sidebar — Harnesses live inside Projects.
Database schema changes are versioned under
backend/internal/db/migrations and applied
automatically by the backend on startup using goose.
| Target | What it does |
|---|---|
make dev |
Start backend (:8080) and frontend (:3000) together |
make dev-back |
Backend only, sources backend/.env |
make dev-front |
Frontend dev server (bun dev) |
make build |
Compile the Go server to backend/bin/server |
make stop |
Kill processes on :8080 and :3000 |
make build-harness |
Build all harness Docker images; pass HARNESS=<name> to build one |
make build-pod |
Build all standalone Pod images (filemgr, skillmgr); pass POD=<name> to build one |
Backend env vars (file: backend/.env, gitignored):
| Variable | Default | Purpose |
|---|---|---|
DATABASE_URL |
postgres://postgres@localhost:5432/cattery?sslmode=disable |
Postgres DSN |
PORT |
8080 |
HTTP listen port |
K8S_NAMESPACE |
default |
Namespace where Sandbox CRs are created |
ANTHROPIC_BASE_URL |
— | Anthropic-compatible API base URL |
ANTHROPIC_API_KEY |
— | Auth token for Anthropic models |
OPENAI_BASE_URL |
— | OpenAI-compatible API base URL, including /v1 |
OPENAI_API_KEY |
— | Auth token for OpenAI models |
JWT_SECRET |
— | Required. Signing key for login tokens. Use openssl rand -hex 32. Rotating it invalidates every issued token. |
The backend uses clientcmd.RecommendedHomeFile (~/.kube/config) when not running in-cluster.
Cattery has username-password login with a JWT session token. Users are admin-managed — there is no self-signup.
First-time setup: on the very first start (when the users table is empty), the server auto-creates an admin account admin with a random password and logs it once:
================================================================
[auth] First-time admin account created:
[auth] username: admin
[auth] password: WEUUW-WCZXM-M4WNS-6RWAQ
[auth] Sign in and change the password from the user menu.
[auth] This message will NOT appear again.
================================================================
Capture the password from the logs, sign in, and change it. Lost the password? Reset it in Postgres (UPDATE users SET password_hash = '<bcrypt>' WHERE username = 'admin') or wipe the users table to trigger a fresh bootstrap.
Adding more users: log in as admin → user menu → "User management" → "Add user". Users can change their own password from the user menu.
Token lifetime is 7 days, with no server-side revocation (no session table). Two practical implications:
- Logging out only clears the token from the browser. The token itself stays valid until expiry — keep
JWT_SECRETconfidential. - Admin role changes propagate immediately for the affected user's next HTTP request that depends on
/auth/me, but/admin/*endpoints accept the token's claim until it expires. Plan up to a 7-day window for full role-change propagation. To shorten this, lower the TTL ininternal/auth/jwt.goor rotateJWT_SECRET(kicks everyone out).
Deleting a user cascades to their harnesses and sessions, and stops their K8s sandboxes; their existing token also becomes unusable on the next /auth/me probe (the frontend re-validates every 60 s).
- Project — a shared workspace owned by one user with optional members at
owner/memberroles. Each project has its own PVC-backed file workspace, served by a per-projectfilemgrPod. - Harness — an agent template (model, prompt, harness_id, repo, env_vars) scoped to a Project. Owns a single long-lived sandbox.
- Sandbox — one Kubernetes
agents.x-k8s.io/v1alpha1Sandbox CR per Harness, namedcattery-<harness_id>. Status is mirrored to the Harness row. - Session — a conversation inside a Harness's sandbox. Multiple sessions share one sandbox.
- Skill — a
<slug>/SKILL.md (+assets)directory in the global skills library. Skills are cluster-wide, independent of Projects, served by a singleskillmgrPod and read-only mounted into every harness sandbox at the path that harness auto-discovers (e.g.~/.claude/skills,~/.agents/skills).
Project members see all of the project's harnesses, files, and sessions; their access role gates which mutations they can perform. Deleting a project tears down its filemgr Pod, all harness sandboxes, and the workspace PVC.
GET /healthz— liveness; always 200 once the process is up.GET /readyz— readiness; reports DB and Kubernetes client wiring.
Both probes are excluded from the request log to keep K8s polling traffic out of stdout.
Pick a transport kind in backend/internal/harness/registry.go:
Implement this contract on agent.container_port (default 4096):
POST /session → { id }
POST /session/:id/prompt_async → 204
GET /session/:id/message → history
POST /session/:id/abort
GET /event → SSE stream of harness-native events
Add a subpackage at backend/internal/harness/<name>/ with translator.go (stream) and history.go (replay) that emit PlatformEvents, then call harness.Register(id, stream, history) from init(). See harness/opencode/ for a reference.
For TUI-style agents, just call harness.RegisterTerminal(id) from init() — the session is served at GET /api/v1/sessions/:id/term and bytes are proxied both directions. See harness/codex/register.go and harness/hermes/register.go.
See CLAUDE.md for the full protocol spec.
Internal project — not yet licensed for public use.