Agent deployment platform on Kubernetes. Deploy, manage, and monitor AI agents at scale.
Live demo: ranch.cleanslice.org
Built with CleanSlice architecture.
Install the CLI globally — it auto-clones the project on first run:
bun add -g @cleanslice/ranch
# or: npm install -g @cleanslice/ranch
ranch dev # offers to clone if no checkout, then starts api + app + admin + local k3dSee cli/README.md for all ranch commands.
git clone https://github.com/CleanSlice/ranch.git
cd ranch
make initThe setup wizard will guide you through:
- Installing dependencies (Bun, Docker)
- Setting up local PostgreSQL and running migrations
- Optionally: configuring and deploying to Hetzner Cloud
After setup, start developing:
make dev # Starts api:3000 + app:3001 + admin:3002┌─────────────────────────────────────────────────────────┐
│ Hetzner Cloud │
│ │
│ ┌────────────┐ ┌────────────────┐ ┌───────────────┐ │
│ │ ArgoCD │ │ Argo Workflows │ │ CloudNativePG│ │
│ │ (GitOps) │ │ (Agent runs) │ │ (PostgreSQL) │ │
│ └─────┬──────┘ └──────▲─────────┘ └───────────────┘ │
│ │ sync │ submit │
│ ▼ │ │
│ ┌──────────────────────┴──────────────────────────┐ │
│ │ Agent Manager (api) │ │
│ │ NestJS + Prisma + CleanSlice │ │
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ Dashboard (app)│ │ Admin Panel (admin) │ │
│ │ Nuxt + Vue 3 │ │ Nuxt + Vue 3 │ │
│ └─────────────────┘ └─────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Agent Pods (dynamic, via Argo Workflows) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
| Layer | Technology |
|---|---|
| Backend | NestJS, Prisma, PostgreSQL |
| Frontend | Nuxt 3, Vue 3, Pinia, Tailwind, shadcn-vue |
| Infrastructure | Terraform, Hetzner Cloud, k3s |
| Deployment | ArgoCD (GitOps), Argo Workflows |
| Runtime | Bun |
| Monorepo | Turborepo |
An agent can hand work to another agent. Nothing is copied between them: each keeps its own runtime, tools and knowledge. What travels is a card — the agent-to-agent equivalent of a business card, saying who an agent is and what it can do. Connect a card to an agent and it gains a colleague it can ask.
The mental model: MCP is the tools in an agent's hands; A2A is the colleagues at the next desk. Copying an agent is a different feature — that is templates.
- The card is derived, never written by hand: an agent's name and
description, its template's skills, and the knowledge bases bound to it.
Edit any of those and the card follows. It is served at the A2A standard
path,
/a2a/agents/<id>/.well-known/agent-card.json, and needs a credential — there is no anonymous discovery. - Connecting a peer happens in the admin console, on an agent's A2A
tab (formerly Peers — old
?tab=peerslinks still land). Pick another agent of this ranch, or import an external A2A agent by its address — paste the URL, read its card, optionally give the access credential it expects, confirm. Re-importing the same address updates the entry in place. Connections are one-way: connecting B to A says nothing about what B may ask of A. - Delegating is a tool the API serves to the runtime, so no agent image changes. A pod reads its tool list once at boot, so a new peer needs a restart — the tab shows armed / pending restart and the banner carries a one-click Restart now. The tool also carries the delegation policy: an agent that cannot answer must try a plausible peer before saying "I don't know".
- Watching it happen: a delegation appears in the chat's thinking timeline while it runs, naming the peer, what its card promised, why it was chosen and how long the wait is. Past delegations are listed on the A2A tab; external peers are marked in both places.
- Limits: a chain may be three hops long; an agent already in a chain
refuses to re-enter it. External addresses must be publicly reachable —
private hosts are refused (
A2A_ALLOW_PRIVATE_PEERS=truelifts that for local development).
Set PUBLIC_API_URL to the origin other agents reach this API on — every
card URL is built from it. See specs/013-a2a-agent-peers/ and
specs/014-a2a-protocol-upgrade/ for the full specifications.
ranch/
├── api/ # NestJS backend
│ └── src/slices/
│ ├── setup/ # prisma, init, health
│ ├── user/ # auth + user
│ ├── agent/ # agent, file, pod, secret, template, templateFile, peer
│ ├── workflow/ # Argo Workflows integration
│ ├── bridle/ # chat (sessions, messages, streaming)
│ ├── mcp/ # MCP runtime hosted at /mcp/*
│ ├── mcpServer/ # MCP server registry
│ ├── llm/ # LLM provider config
│ ├── skill/ # Reusable agent skills
│ ├── rancher/ # Per-tenant ranch (workspace) management
│ ├── reins/ # Access control / API keys
│ ├── setting/ # System settings
│ ├── usage/ # Usage metering
│ ├── log/ # Agent logs
│ └── aws/ # AWS-backed integrations (S3, etc.)
├── app/ # Nuxt user dashboard
│ └── slices/ # setup, agent, bridle, share, template, user, common
├── admin/ # Nuxt admin panel
│ └── slices/ # setup, agent (incl. peer), bridle, llm, mcpServer,
│ # rancher,
│ # reins, setting, skill, usage, user, common
├── cli/ # @cleanslice/ranch — published to npm
├── rancher/ # Default agent template ("rancher" agent)
├── terraform/ # Hetzner infrastructure
│ ├── modules/ # cluster, network, bootstrap, dns, database, storage, apps
│ └── environments/ # dev, prod, dreamvention (+ your custom env)
├── k8s/ # Kubernetes manifests
│ ├── argocd/ # App-of-apps
│ ├── infrastructure/ # Argo Workflows, CNPG, database
│ ├── platform/ # api, app, admin deployments
│ ├── templates/ # Argo WorkflowTemplate + RBAC for agents
│ ├── local/ # Local k3d-only manifests (CoreDNS host alias, etc.)
│ └── deploy/ # Deploy hooks
└── .github/workflows/ # CI/CD
- Bun —
curl -fsSL https://bun.sh/install | bash - Docker — for local PostgreSQL
- Terraform — for infrastructure
- kubectl — for cluster management
- Helm — for chart management
The ranch CLI wraps the most-used make targets and works from any
directory once installed globally. See cli/README.md
for the full reference.
bun add -g @cleanslice/ranch # one-time install
ranch dev # start api + app + admin
ranch dev api # or just one service: api | app | admin
ranch down # stop dev servers, postgres, k3d
ranch db start | stop | reset | studio
ranch generate # regenerate Prisma schema from slices
ranch status # show what's running locally
ranch where # show which Ranch checkout the CLI is usingEquivalent targets, run from the project root:
make help # Show all commands
# Development
make setup # Full local setup (first time)
make dev # Start all services (api:3000, app:3001, admin:3002)
make dev-api # Start API only
make dev-app # Start app only
make dev-admin # Start admin only
# Database
make db # Start PostgreSQL via Docker
make db-stop # Stop PostgreSQL
make db-reset # Reset database (destroy data)
make migrate # Generate schema + run migrations
make generate # Regenerate schema.prisma from slices
make studio # Open Prisma Studio
# Build & Test
make build # Build all services
make lint # Lint all services
make test # Run tests
make clean # Remove node_modules and build artifactsAfter ranch dev (or make dev):
| URL | Description |
|---|---|
http://localhost:3000 |
API (NestJS) |
http://localhost:3000/api |
Swagger UI — full endpoint reference |
http://localhost:3000/mcp/* |
MCP runtime (tools exposed via @Tool decorators) |
http://localhost:3000/health |
Health check |
http://localhost:3001 |
App (user dashboard) |
http://localhost:3002 |
Admin panel |
ranch dev also brings up a local k3d cluster (ranch) with the
platform and agents namespaces and Argo workflow templates pre-applied,
so the API can submit real agent workflows against a local Kubernetes.
Kubeconfig is written to ~/.kube/ranch-local.yaml.
For RAG-backed agents, a LightRAG + Ollama + Postgres stack ships behind a Docker Compose profile:
make lightrag-up # First run pulls ~3GB of models — LightRAG at :9621
make lightrag-down # Stop (keeps data)
make lightrag-reset # Wipe data + models
make lightrag-logs # Tail container logsEvery agent page has a Share action that mints one public link for that
agent: <console origin>/share?token=sl_…. Opening it gives a full-view chat
with that agent and nothing else — no login, no navigation, no console pages.
- Share is idempotent: an agent that already has an active link hands the same one back, so pressing it twice never breaks a link in circulation.
- Regenerate mints a new token and kills the old one in the same write — the link stays on, the old URL stops working. Revoke turns the link off entirely. Nothing else ends a token's life: links never expire on their own, and an open visitor page notices it was cut off within 30 seconds.
- Visitor requests carry
X-Share-Token(the secret) andX-Share-Visitor(an opaque per-browser id) instead of a bearer token, and the console explicitly drops its ownAuthorizationheader on them so an owner previewing their link is treated as a visitor. The API re-validates the pair against the agent in the path on every request. - Each visitor gets their own conversation, stored on the channel
share-<visitorId>, so share traffic shows up in the agent's chat history for console users and is recognisable by that prefix. Attachments are own-only: a visitor can read back the files they uploaded to their own chat, nothing else. - A token grants exactly that one chat — only the agent name and status, no other agents, no console routes.
The AgentShareLink table ships in the hand-written migration
api/prisma/migrations/20260907120000_agent_share_link (additive: a new table
plus a cascading FK, safe on an existing database).
Signing in to either console establishes two credentials with different jobs:
- a session — an opaque
rs_…secret the API stores hashed in theSessiontable and hands to the browser as an httpOnly cookieranch_sessionscoped toPath=/auth(so it never rides on ordinary requests). It has a sliding inactivity window (SESSION_IDLE_DAYS, 7) and a hard ceiling (SESSION_ABSOLUTE_DAYS, 30);SecurefollowsSESSION_COOKIE_SECURE(true in the cluster, false for plain-http dev); - a short-lived access token — the JWT the console attaches as
Authorization: Bearer(JWT_EXPIRES_IN, 15 minutes), kept in memory only.
POST /auth/refresh (no body, cookie in) renews the access token from the
session alone — it works after the token has expired — and slides the
inactivity window. POST /auth/logout revokes the session and clears the
cookie. The consoles renew proactively a minute before expiry, when a hidden
tab becomes visible, and once more reactively when a request comes back 401,
then retry that request once; when the session itself cannot be renewed they
show a single in-place "Your session has ended" dialog with the login form,
leaving the page and any unsent message intact.
Every 401 carries a machine-readable code:
code |
Meaning | Console behaviour |
|---|---|---|
TOKEN_MISSING |
no bearer | logged out |
TOKEN_EXPIRED |
bearer exp passed |
refresh, retry once |
TOKEN_INVALID |
bad signature / unusable subject | refresh once, then session ended |
SESSION_MISSING / SESSION_EXPIRED / SESSION_INVALID |
from /auth/refresh |
session ended (silently logged out on boot) |
The bridle chat routes reject a failing bearer the same way instead of quietly
treating the caller as an anonymous visitor; a request with no credentials
still gets the anonymous throwaway channel, and share-link headers still win
over a dead console token. Agent service, embed and browser-extension tokens
set their own lifetimes and are unaffected. Contract and rationale:
specs/012-jwt-token-refresh/contracts/session-api.md.
The user console ships in English and Russian. en.json in each slice is the
source; bun run i18n:sync generates the translation, and bun run i18n:check
(a CI step) fails on a missing, stale or undefined key. The admin panel is
English-only by design.
See docs/i18n.md before adding a user-visible string.
Each slice defines its own .prisma file at the slice root. They are merged into prisma/schema.prisma by prisma-import at build time.
api/src/slices/
├── setup/prisma/prisma.prisma # datasource + generator
├── agent/agent/agent.prisma # Agent model
├── agent/template/template.prisma # Template model
└── user/user/user.prisma # User model
Edit the slice .prisma files, then:
make generate # Merge into prisma/schema.prisma
make migrate # Create migration + generate client📘 Full Terraform guide (remote state backend, environments, imports, DNS, gotchas):
terraform/README.md
# Copy the dev template
cp -r terraform/environments/dev terraform/environments/myenv
# Edit variables
vi terraform/environments/myenv/terraform.tfvars.example
# Copy and fill in your values:
cp terraform/environments/myenv/terraform.tfvars.example terraform/environments/myenv/terraform.tfvarsRequired variables in terraform.tfvars:
hcloud_token = "your-hetzner-api-token"
domain = "ranch.yourdomain.com"
admin_ip = "YOUR_IP/32" # curl ifconfig.me
ssh_public_key_path = "~/.ssh/id_ed25519.pub"The k3s cluster requires MicroOS snapshots on Hetzner:
brew install packer # if not installed
# Download and build snapshots
cd /tmp
curl -sLO https://raw.githubusercontent.com/kube-hetzner/terraform-hcloud-kube-hetzner/master/packer-template/hcloud-microos-snapshots.pkr.hcl
packer init hcloud-microos-snapshots.pkr.hcl
HCLOUD_TOKEN="your-token" packer build hcloud-microos-snapshots.pkr.hclmake infra-init ENV=myenv # Download providers
make infra-plan ENV=myenv # Preview changes
make infra-apply ENV=myenv # Create cluster (~5-10 min)This creates:
- k3s cluster with Traefik ingress and cert-manager
- VPC + Firewall
- Load Balancer
make kubeconfig ENV=myenv
export KUBECONFIG=~/.kube/ranch-myenv
kubectl get nodes# Add Helm repos
helm repo add argo https://argoproj.github.io/argo-helm
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
# Install ArgoCD
helm install argocd argo/argo-cd \
--namespace argocd --create-namespace \
--set server.ingress.enabled=true \
--set server.ingress.ingressClassName=traefik \
--set "server.ingress.hosts[0]=argocd.ranch.yourdomain.com"
# Install Argo Workflows
helm install argo-workflows argo/argo-workflows \
--namespace argo --create-namespace
# Install CloudNativePG
helm install cnpg cnpg/cloudnative-pg \
--namespace cnpg-system --create-namespace
# Create namespaces
kubectl create namespace platform
kubectl create namespace agentsRanch ships five public services behind one shared Hetzner Load Balancer. You need an A record per subdomain at your DNS provider — the records themselves are not managed by Terraform (intentional: keeps the DNS zone your single source of truth and lets you change provider without touching the platform module).
Required A records — point each at the same Load Balancer IP:
| Subdomain | Serves | Required? |
|---|---|---|
ranch.yourdomain.com |
App (root, end-user UI) | yes |
api.ranch.yourdomain.com |
NestJS API | yes |
admin.ranch.yourdomain.com |
Admin panel (Nuxt) | yes |
argocd.ranch.yourdomain.com |
ArgoCD UI | yes |
browser.ranch.yourdomain.com |
Browser-pool live VNC view | yes — agents send users this URL for manual logins (Instagram, Meta Ads, etc.). Without it, every site that needs an interactive login is unreachable. |
Get the Load Balancer IP after terraform apply:
kubectl get svc -n traefik traefik -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
# or, equivalently, ask Terraform:
terraform -chdir=terraform/environments/<env> output dns_records_neededThe dns_records_needed output lists every subdomain that needs a record
and the IP to point it at — keep this in sync if you ever add a new
public-facing ingress (otherwise cert-manager will spin forever on a
challenge that can't resolve).
Propagation: 1–5 min typical (set TTL=300). cert-manager then
issues a Let's Encrypt cert automatically via the HTTP-01 challenge —
the first hit to https://<subdomain>/ after propagation triggers it.
Smoke test once propagated:
for sub in "" api. admin. argocd. browser.; do
host="${sub}ranch.yourdomain.com"
printf "%-45s " "$host"
curl -sS -o /dev/null -w "%{http_code}\n" "https://$host/" --max-time 5 || echo "(timeout)"
doneAll five should return a non-zero HTTP code (200/301/302/401 are all fine — means the cert handshake succeeded and Traefik routed the request).
# Get ArgoCD password
make argocd-password
# Login to ArgoCD UI at https://argocd.ranch.yourdomain.com
# Username: admin
# Password: <output from above>
# Deploy app-of-apps
make deploymake k8s-secrets # Interactive — will ask for DATABASE_URLRanch is designed to be forked per-organization:
# 1. Fork CleanSlice/ranch → YourOrg/ranch
# 2. Clone your fork
git clone git@github.com:YourOrg/ranch.git
cd ranch
# 3. Add upstream
git remote add upstream git@github.com:CleanSlice/ranch.git
# 4. Create your environment
cp -r terraform/environments/dev terraform/environments/yourenv
# 5. Customize k8s manifests
# - k8s/argocd/app-of-apps.yaml → change repoURL to YourOrg/ranch
# - k8s/platform/*/ingress.yaml → change hostnames
# - k8s/platform/*/deployment.yaml → change image registry
# - Makefile → change ENV default
# 6. Sync upstream changes
git fetch upstream
git merge upstream/main
git push origin mainmake k8s-status # Show nodes, pods, ArgoCD apps
make k8s-logs-api # Tail API logs
make argocd-password # Get ArgoCD admin password
make infra-destroy ENV=myenv # Tear down infrastructuregit push → GitHub Actions:
├── CI (lint + test)
├── Build Docker images → ghcr.io
└── ArgoCD detects new image
└── Syncs to cluster (zero-downtime rolling update)
ArgoCD manages:
- Infrastructure — Argo Workflows, CloudNativePG, PostgreSQL cluster
- Platform — API, App, Admin deployments
- Templates — Argo WorkflowTemplates for agent pods
Private — CleanSlice
