From 9da8546d19fa4980d002cdd58ed12c83d992152e Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 20 Sep 2026 14:34:57 +0700 Subject: [PATCH] feat(goclaw): add multi-tenant AI agent gateway with pgvector postgres --- README.md | 1 + goclaw/.env.example | 22 +++++++++ goclaw/README.md | 109 ++++++++++++++++++++++++++++++++++++++++++++ goclaw/compose.yml | 62 +++++++++++++++++++++++++ 4 files changed, 194 insertions(+) create mode 100644 goclaw/.env.example create mode 100644 goclaw/README.md create mode 100644 goclaw/compose.yml diff --git a/README.md b/README.md index da9e4f0..01b33ec 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ Each links to its own README for variables, ports, and storage. | [couchbase](couchbase/README.md) | Couchbase Server | | [diun](diun/README.md) | Image-update notifier, reading the Docker API through a read-only proxy | | [gitea-mirror](gitea-mirror/README.md) | Gitea + PostgreSQL + gitea-mirror, mirroring GitHub repos | +| [goclaw](goclaw/README.md) | Multi-tenant AI agent gateway, with pgvector PostgreSQL | | [opencode-web](opencode-web/README.md) | opencode coding agent, served as a browser UI | | [paseo](paseo/README.md) | Paseo coding-agent daemon and web UI | | [traffmonetizer](traffmonetizer/README.md) | TraffMonetizer bandwidth-sharing client | diff --git a/goclaw/.env.example b/goclaw/.env.example new file mode 100644 index 0000000..d41376c --- /dev/null +++ b/goclaw/.env.example @@ -0,0 +1,22 @@ +# Copy to .env and fill in. Never commit .env. +# +# cp .env.example .env + +# Bearer token for the gateway API and dashboard. MUST NOT be blank -- a blank +# token serves the agent gateway, and the tools its agents can run, to anyone. +# Generate one with: openssl rand -hex 16 +GOCLAW_GATEWAY_TOKEN= + +# AES-256-GCM key encrypting the LLM provider keys stored in PostgreSQL. +# Changing it makes every stored key unreadable. +# Generate one with: openssl rand -hex 32 +GOCLAW_ENCRYPTION_KEY= + +# PostgreSQL credentials. Used by the database and by the DSN GoClaw connects +# with, so both sides move together. +POSTGRES_USER=goclaw +POSTGRES_PASSWORD=goclaw +POSTGRES_DB=goclaw + +# Set to 1 to log full LLM request and response bodies. +GOCLAW_TRACE_VERBOSE=0 diff --git a/goclaw/README.md b/goclaw/README.md new file mode 100644 index 0000000..26057d2 --- /dev/null +++ b/goclaw/README.md @@ -0,0 +1,109 @@ +# goclaw + +[GoClaw](https://docs.goclaw.sh) is a multi-tenant AI agent gateway in Go: LLM +providers, messaging channels (Telegram, Discord, Slack, Zalo, Feishu, +WhatsApp), agent teams, and a knowledge vault, behind one binary with a +built-in web dashboard. + +Two containers: `goclaw`, and `postgres` — pgvector, which holds tenants, +sessions, encrypted provider keys and the semantic memory. + +## Setup + +1. Generate the two secrets and set them, with the rest of `.env.example`, in + Coolify or Dokploy: + + ```sh + openssl rand -hex 16 # GOCLAW_GATEWAY_TOKEN + openssl rand -hex 32 # GOCLAW_ENCRYPTION_KEY + ``` + +2. Point the domain at port `18790` and deploy. Migrations run on start. +3. Open the domain and use the setup wizard to add an LLM provider key. + +Health check: `GET /health`. + +## Authentication + +`GOCLAW_GATEWAY_TOKEN` is the bearer token for the API and the dashboard, and +the only thing between the domain and a gateway whose agents run tools. It +fails fast if unset rather than defaulting to blank: upstream's own compose +allows an empty token for loopback-only development, which behind a public +domain would publish the gateway. + +`GOCLAW_ENCRYPTION_KEY` encrypts the provider API keys at rest in PostgreSQL +(AES-256-GCM). It is required for the same reason, and it must not change once +keys are stored — every one of them becomes unreadable. + +Both are 1:1 with what `prepare-env.sh` generates upstream; that script is not +used here because the platform owns the environment. + +## Environment + +| Variable | Default | Purpose | +| --- | --- | --- | +| `GOCLAW_GATEWAY_TOKEN` | — | Bearer token for the API and dashboard | +| `GOCLAW_ENCRYPTION_KEY` | — | AES-256-GCM key for stored provider keys | +| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | `goclaw` | Database credentials, read by both containers | +| `GOCLAW_TRACE_VERBOSE` | `0` | Log full LLM request and response bodies | + +`GOCLAW_HOST`, `GOCLAW_PORT`, `GOCLAW_CONFIG` and `GOCLAW_SKILLS_DIR` are set +in `compose.yml` rather than here — they are properties of this layout, not of +whoever deploys it. The two paths override image defaults that point outside +any volume (`/app/config.json`, `/app/skills`), which would drop the +configuration and every installed skill on redeploy. + +Provider API keys are not variables: they are entered in the dashboard and +stored encrypted in PostgreSQL. + +## Storage + +| Volume | Mount | Holds | +| --- | --- | --- | +| `goclaw-data` | `/app/data` | `config.json`, skills, Claude CLI credentials, runtime packages | +| `goclaw-workspace` | `/app/workspace` | Files the agents work on | +| `postgres-data` | `/var/lib/postgresql` | The database | + +Upstream's PostgreSQL overlay adds a third `goclaw-skills` volume at +`/app/skills`; it is not here, because `GOCLAW_SKILLS_DIR` points at +`/app/data/skills` and the volume would mount empty over a path nothing reads. +Skills persist in `goclaw-data` instead. + +`postgres-data` mounts `/var/lib/postgresql`, not `/var/lib/postgresql/data`. +PostgreSQL 18 images moved `PGDATA` down a level into a version directory, so +the parent is now the mount point. + +The image's own user owns `/app` — `goclaw`, uid 1000, created with `/app` as +its home. `/app/.claude` is a symlink into `/app/data`, which is how CLI +credentials survive a recreate. + +## Networking + +Listens on `18790`, published nowhere — the platform maps the domain to it. See +the [root README](../README.md) for why. + +`extra_hosts` maps `host.docker.internal` to the host gateway, so an agent can +reach a service running on the host itself. + +## Image + +`ghcr.io/nextlevelbuilder/goclaw:full`, the variant with every runtime and +skill dependency pre-installed — Python, Node, pandoc, poppler, the GitHub CLI. +The plain tag ships Python only and installs the rest on demand into +`/app/data/.runtime`, which means the first use of a skill waits on a package +install and a wiped data volume repeats it. Paying for the size once is the +better trade here. + +The tag is moving, tracking the newest full build. The variant tags are only +published as `full` and `.-full` — there is no `-full` — +so `full` is the moving tag to follow. + +## Hardening + +`cap_drop: ALL` with `SETUID`/`SETGID`/`CHOWN` added back, `no-new-privileges`, +a `noexec` tmpfs on `/tmp`, and memory, CPU and pid limits — all as upstream +ships them. The three capabilities are what the entrypoint needs to install +persisted packages as root and then drop to the `goclaw` user via `su-exec`. + +This is a service whose whole purpose is running model-chosen tools, so the +limits are worth keeping even though nothing else here sets them. diff --git a/goclaw/compose.yml b/goclaw/compose.yml new file mode 100644 index 0000000..90f80e5 --- /dev/null +++ b/goclaw/compose.yml @@ -0,0 +1,62 @@ +# Required env vars (set in Coolify/Dokploy, or a sibling .env): +# GOCLAW_GATEWAY_TOKEN, GOCLAW_ENCRYPTION_KEY + +services: + goclaw: + image: ghcr.io/nextlevelbuilder/goclaw:full + restart: unless-stopped + environment: + - GOCLAW_GATEWAY_TOKEN=${GOCLAW_GATEWAY_TOKEN:?required} + - GOCLAW_ENCRYPTION_KEY=${GOCLAW_ENCRYPTION_KEY:?required} + - GOCLAW_POSTGRES_DSN=postgres://${POSTGRES_USER:-goclaw}:${POSTGRES_PASSWORD:-goclaw}@postgres:5432/${POSTGRES_DB:-goclaw}?sslmode=disable + - GOCLAW_HOST=0.0.0.0 + - GOCLAW_PORT=18790 + - GOCLAW_CONFIG=/app/data/config.json + - GOCLAW_SKILLS_DIR=/app/data/skills + - GOCLAW_TRACE_VERBOSE=${GOCLAW_TRACE_VERBOSE:-0} + volumes: + - goclaw-data:/app/data + - goclaw-workspace:/app/workspace + depends_on: + postgres: + condition: service_healthy + extra_hosts: + - host.docker.internal:host-gateway + init: true + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - SETUID + - SETGID + - CHOWN + tmpfs: + - /tmp:rw,noexec,nosuid,size=256m + deploy: + resources: + limits: + memory: 1G + cpus: '2.0' + pids: 200 + + # PostgreSQL 18 with pgvector, for agent memory and the knowledge vault. + postgres: + image: pgvector/pgvector:pg18 + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-goclaw} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-goclaw} + POSTGRES_DB: ${POSTGRES_DB:-goclaw} + volumes: + - postgres-data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-goclaw} -d ${POSTGRES_DB:-goclaw}"] + interval: 5s + timeout: 5s + retries: 10 + +volumes: + goclaw-data: + goclaw-workspace: + postgres-data: