Skip to content

wellCh4n/cattery

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

144 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cattery

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
Loading

Prerequisites

  • 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/v1alpha1 Sandbox CRD that Cattery uses to launch isolated agent pods
  • PostgreSQL 14+ — run it yourself, or let the bundled docker-compose.yml start 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)

Install the cluster prerequisites

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-system

See the Agent Sandbox Getting Started guide for the controller's own RBAC, namespacing, and extension components.

Quick start

# 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 below

Pick 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 web

Open 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.

Make targets

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

Configuration

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.

Authentication

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_SECRET confidential.
  • 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 in internal/auth/jwt.go or rotate JWT_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).

Resource model

  • Project — a shared workspace owned by one user with optional members at owner / member roles. Each project has its own PVC-backed file workspace, served by a per-project filemgr Pod.
  • 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/v1alpha1 Sandbox CR per Harness, named cattery-<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 single skillmgr Pod 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.

Operational endpoints

  • 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.

Adding a new harness

Pick a transport kind in backend/internal/harness/registry.go:

HTTP harness (translated SSE)

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.

Terminal harness (raw PTY over WebSocket)

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.

License

Internal project — not yet licensed for public use.

About

Cattery: Multi-harness sandbox for harness — opencode, Claude Code, Codex and Hermes, each in its own isolated Kubernetes sandbox.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors