revert: Switch back to tsc for compiling.

This commit is contained in:
cpojer
2026-01-31 18:31:49 +09:00
parent e25fedf932
commit 76361ae3ab
36 changed files with 527 additions and 843 deletions

View File

@@ -1,5 +1,5 @@
---
summary: "Optional Docker-based setup and onboarding for OpenClaw"
summary: 'Optional Docker-based setup and onboarding for OpenClaw'
read_when:
- You want a containerized gateway instead of local installs
- You are validating the Docker flow
@@ -16,6 +16,7 @@ Docker is **optional**. Use it only if you want a containerized gateway or to va
- **Sandboxing note**: agent sandboxing uses Docker too, but it does **not** require the full gateway to run in Docker. See [Sandboxing](/gateway/sandboxing).
This guide covers:
- Containerized Gateway (full OpenClaw in Docker)
- Per-session Agent Sandbox (host gateway + Docker-isolated agent tools)
@@ -37,6 +38,7 @@ From repo root:
```
This script:
- builds the gateway image
- runs the onboarding wizard
- prints optional provider setup hints
@@ -44,15 +46,18 @@ This script:
- generates a gateway token and writes it to `.env`
Optional env vars:
- `OPENCLAW_DOCKER_APT_PACKAGES` — install extra apt packages during build
- `OPENCLAW_EXTRA_MOUNTS` — add extra host bind mounts
- `OPENCLAW_HOME_VOLUME` — persist `/home/node` in a named volume
After it finishes:
- Open `http://127.0.0.1:18789/` in your browser.
- Paste the token into the Control UI (Settings → token).
It writes config/workspace on the host:
- `~/.openclaw/`
- `~/.openclaw/workspace`
@@ -81,6 +86,7 @@ export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/ho
```
Notes:
- Paths must be shared with Docker Desktop on macOS/Windows.
- If you edit `OPENCLAW_EXTRA_MOUNTS`, rerun `docker-setup.sh` to regenerate the
extra compose file.
@@ -110,6 +116,7 @@ export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/ho
```
Notes:
- If you change `OPENCLAW_HOME_VOLUME`, rerun `docker-setup.sh` to regenerate the
extra compose file.
- The named volume persists until removed with `docker volume rm <name>`.
@@ -129,6 +136,7 @@ export OPENCLAW_DOCKER_APT_PACKAGES="ffmpeg build-essential"
```
Notes:
- This accepts a space-separated list of apt package names.
- If you change `OPENCLAW_DOCKER_APT_PACKAGES`, rerun `docker-setup.sh` to rebuild
the image.
@@ -163,7 +171,7 @@ RUN pnpm ui:build
ENV NODE_ENV=production
CMD ["node","dist/index.mjs"]
CMD ["node","dist/index.js"]
```
### Channel setup (optional)
@@ -171,16 +179,19 @@ CMD ["node","dist/index.mjs"]
Use the CLI container to configure channels, then restart the gateway if needed.
WhatsApp (QR):
```bash
docker compose run --rm openclaw-cli channels login
```
Telegram (bot token):
```bash
docker compose run --rm openclaw-cli channels add --channel telegram --token "<token>"
```
Discord (bot token):
```bash
docker compose run --rm openclaw-cli channels add --channel discord --token "<token>"
```
@@ -190,7 +201,7 @@ Docs: [WhatsApp](/channels/whatsapp), [Telegram](/channels/telegram), [Discord](
### Health check
```bash
docker compose exec openclaw-gateway node dist/index.mjs health --token "$OPENCLAW_GATEWAY_TOKEN"
docker compose exec openclaw-gateway node dist/index.js health --token "$OPENCLAW_GATEWAY_TOKEN"
```
### E2E smoke test (Docker)
@@ -218,6 +229,7 @@ Deep dive: [Sandboxing](/gateway/sandboxing)
When `agents.defaults.sandbox` is enabled, **non-main sessions** run tools inside a Docker
container. The gateway stays on your host, but the tool execution is isolated:
- scope: `"agent"` by default (one container + workspace per agent)
- scope: `"session"` for per-session isolation
- per-scope workspace folder mounted at `/workspace`
@@ -233,6 +245,7 @@ one container and one workspace.
If you use multi-agent routing, each agent can override sandbox + tool settings:
`agents.list[].sandbox` and `agents.list[].tools` (plus `agents.list[].tools.sandbox.tools`). This lets you run
mixed access levels in one gateway:
- Full access (personal agent)
- Read-only tools + read-only workspace (family/work agent)
- No filesystem/shell tools (public agent)
@@ -255,60 +268,72 @@ precedence, and troubleshooting.
### Enable sandboxing
If you plan to install packages in `setupCommand`, note:
- Default `docker.network` is `"none"` (no egress).
- `readOnlyRoot: true` blocks package installs.
- `user` must be root for `apt-get` (omit `user` or set `user: "0:0"`).
OpenClaw auto-recreates containers when `setupCommand` (or docker config) changes
unless the container was **recently used** (within ~5 minutes). Hot containers
log a warning with the exact `openclaw sandbox recreate ...` command.
OpenClaw auto-recreates containers when `setupCommand` (or docker config) changes
unless the container was **recently used** (within ~5 minutes). Hot containers
log a warning with the exact `openclaw sandbox recreate ...` command.
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // off | non-main | all
scope: "agent", // session | agent | shared (agent is default)
workspaceAccess: "none", // none | ro | rw
workspaceRoot: "~/.openclaw/sandboxes",
mode: 'non-main', // off | non-main | all
scope: 'agent', // session | agent | shared (agent is default)
workspaceAccess: 'none', // none | ro | rw
workspaceRoot: '~/.openclaw/sandboxes',
docker: {
image: "openclaw-sandbox:bookworm-slim",
workdir: "/workspace",
image: 'openclaw-sandbox:bookworm-slim',
workdir: '/workspace',
readOnlyRoot: true,
tmpfs: ["/tmp", "/var/tmp", "/run"],
network: "none",
user: "1000:1000",
capDrop: ["ALL"],
env: { LANG: "C.UTF-8" },
setupCommand: "apt-get update && apt-get install -y git curl jq",
tmpfs: ['/tmp', '/var/tmp', '/run'],
network: 'none',
user: '1000:1000',
capDrop: ['ALL'],
env: { LANG: 'C.UTF-8' },
setupCommand: 'apt-get update && apt-get install -y git curl jq',
pidsLimit: 256,
memory: "1g",
memorySwap: "2g",
memory: '1g',
memorySwap: '2g',
cpus: 1,
ulimits: {
nofile: { soft: 1024, hard: 2048 },
nproc: 256
nproc: 256,
},
seccompProfile: "/path/to/seccomp.json",
apparmorProfile: "openclaw-sandbox",
dns: ["1.1.1.1", "8.8.8.8"],
extraHosts: ["internal.service:10.0.0.5"]
seccompProfile: '/path/to/seccomp.json',
apparmorProfile: 'openclaw-sandbox',
dns: ['1.1.1.1', '8.8.8.8'],
extraHosts: ['internal.service:10.0.0.5'],
},
prune: {
idleHours: 24, // 0 disables idle pruning
maxAgeDays: 7 // 0 disables max-age pruning
}
}
}
maxAgeDays: 7, // 0 disables max-age pruning
},
},
},
},
tools: {
sandbox: {
tools: {
allow: ["exec", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"],
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
}
}
}
allow: [
'exec',
'process',
'read',
'write',
'edit',
'sessions_list',
'sessions_history',
'sessions_send',
'sessions_spawn',
'session_status',
],
deny: ['browser', 'canvas', 'nodes', 'cron', 'discord', 'gateway'],
},
},
},
}
```
@@ -328,6 +353,7 @@ scripts/sandbox-setup.sh
This builds `openclaw-sandbox:bookworm-slim` using `Dockerfile.sandbox`.
### Sandbox common image (optional)
If you want a sandbox image with common build tooling (Node, Go, Rust, etc.), build the common image:
```bash
@@ -338,7 +364,11 @@ This builds `openclaw-sandbox-common:bookworm-slim`. To use it:
```json5
{
agents: { defaults: { sandbox: { docker: { image: "openclaw-sandbox-common:bookworm-slim" } } } }
agents: {
defaults: {
sandbox: { docker: { image: 'openclaw-sandbox-common:bookworm-slim' } },
},
},
}
```
@@ -355,6 +385,7 @@ This builds `openclaw-sandbox-browser:bookworm-slim` using
an optional noVNC observer (headful via Xvfb).
Notes:
- Headful (Xvfb) reduces bot blocking vs headless.
- Headless can still be used by setting `agents.defaults.sandbox.browser.headless=true`.
- No full desktop environment (GNOME) is needed; Xvfb provides the display.
@@ -366,10 +397,10 @@ Use config:
agents: {
defaults: {
sandbox: {
browser: { enabled: true }
}
}
}
browser: { enabled: true },
},
},
},
}
```
@@ -379,13 +410,14 @@ Custom browser image:
{
agents: {
defaults: {
sandbox: { browser: { image: "my-openclaw-browser" } }
}
}
sandbox: { browser: { image: 'my-openclaw-browser' } },
},
},
}
```
When enabled, the agent receives:
- a sandbox browser control URL (for the `browser` tool)
- a noVNC URL (if enabled and headless=false)
@@ -405,9 +437,9 @@ docker build -t my-openclaw-sbx -f Dockerfile.sandbox .
{
agents: {
defaults: {
sandbox: { docker: { image: "my-openclaw-sbx" } }
}
}
sandbox: { docker: { image: 'my-openclaw-sbx' } },
},
},
}
```
@@ -420,10 +452,12 @@ docker build -t my-openclaw-sbx -f Dockerfile.sandbox .
### Pruning strategy
Two knobs:
- `prune.idleHours`: remove containers not used in X hours (0 = disable)
- `prune.maxAgeDays`: remove containers older than X days (0 = disable)
Example:
- Keep busy sessions but cap lifetime:
`idleHours: 24`, `maxAgeDays: 7`
- Never prune:
@@ -431,8 +465,8 @@ Example:
### Security notes
- Hard wall only applies to **tools** (exec/read/write/edit/apply_patch).
- Host-only tools like browser/camera/canvas are blocked by default.
- Hard wall only applies to **tools** (exec/read/write/edit/apply_patch).
- Host-only tools like browser/camera/canvas are blocked by default.
- Allowing `browser` in sandbox **breaks isolation** (browser runs on host).
## Troubleshooting

View File

@@ -57,7 +57,7 @@ primary_region = "iad"
NODE_OPTIONS = "--max-old-space-size=1536"
[processes]
app = "node dist/index.mjs gateway --allow-unconfigured --port 3000 --bind lan"
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
[http_service]
internal_port = 3000
@@ -78,13 +78,13 @@ primary_region = "iad"
**Key settings:**
| Setting | Why |
|---------|-----|
| `--bind lan` | Binds to `0.0.0.0` so Fly's proxy can reach the gateway |
| `--allow-unconfigured` | Starts without a config file (you'll create one after) |
| `internal_port = 3000` | Must match `--port 3000` (or `OPENCLAW_GATEWAY_PORT`) for Fly health checks |
| `memory = "2048mb"` | 512MB is too small; 2GB recommended |
| `OPENCLAW_STATE_DIR = "/data"` | Persists state on the volume |
| Setting | Why |
| ------------------------------ | --------------------------------------------------------------------------- |
| `--bind lan` | Binds to `0.0.0.0` so Fly's proxy can reach the gateway |
| `--allow-unconfigured` | Starts without a config file (you'll create one after) |
| `internal_port = 3000` | Must match `--port 3000` (or `OPENCLAW_GATEWAY_PORT`) for Fly health checks |
| `memory = "2048mb"` | 512MB is too small; 2GB recommended |
| `OPENCLAW_STATE_DIR = "/data"` | Persists state on the volume |
## 3) Set secrets
@@ -104,6 +104,7 @@ fly secrets set DISCORD_BOT_TOKEN=MTQ...
```
**Notes:**
- Non-loopback binds (`--bind lan`) require `OPENCLAW_GATEWAY_TOKEN` for security.
- Treat these tokens like passwords.
- **Prefer env vars over config file** for all API keys and tokens. This keeps secrets out of `openclaw.json` where they could be accidentally exposed or logged.
@@ -117,12 +118,14 @@ fly deploy
First deploy builds the Docker image (~2-3 minutes). Subsequent deploys are faster.
After deployment, verify:
```bash
fly status
fly logs
```
You should see:
```
[gateway] listening on ws://0.0.0.0:3000 (PID xxx)
[discord] logged in to discord as xxx
@@ -137,6 +140,7 @@ fly ssh console
```
Create the config directory and file:
```bash
mkdir -p /data
cat > /data/openclaw.json << 'EOF'
@@ -194,12 +198,14 @@ EOF
**Note:** With `OPENCLAW_STATE_DIR=/data`, the config path is `/data/openclaw.json`.
**Note:** The Discord token can come from either:
- Environment variable: `DISCORD_BOT_TOKEN` (recommended for secrets)
- Config file: `channels.discord.token`
If using env var, no need to add token to config. The gateway reads `DISCORD_BOT_TOKEN` automatically.
Restart to apply:
```bash
exit
fly machine restart <machine-id>
@@ -210,6 +216,7 @@ fly machine restart <machine-id>
### Control UI
Open in browser:
```bash
fly open
```
@@ -250,12 +257,14 @@ Fly can't reach the gateway on the configured port.
Container keeps restarting or getting killed. Signs: `SIGABRT`, `v8::internal::Runtime_AllocateInYoungGeneration`, or silent restarts.
**Fix:** Increase memory in `fly.toml`:
```toml
[[vm]]
memory = "2048mb"
```
Or update an existing machine:
```bash
fly machine update <machine-id> --vm-memory 2048 -y
```
@@ -269,6 +278,7 @@ Gateway refuses to start with "already running" errors.
This happens when the container restarts but the PID lock file persists on the volume.
**Fix:** Delete the lock file:
```bash
fly ssh console --command "rm -f /data/gateway.*.lock"
fly machine restart <machine-id>
@@ -281,6 +291,7 @@ The lock file is at `/data/gateway.*.lock` (not in a subdirectory).
If using `--allow-unconfigured`, the gateway creates a minimal config. Your custom config at `/data/openclaw.json` should be read on restart.
Verify the config exists:
```bash
fly ssh console --command "cat /data/openclaw.json"
```
@@ -299,6 +310,7 @@ fly sftp shell
```
**Note:** `fly sftp` may fail if the file already exists. Delete first:
```bash
fly ssh console --command "rm /data/openclaw.json"
```
@@ -332,10 +344,10 @@ If you need to change the startup command without a full redeploy:
fly machines list
# Update command
fly machine update <machine-id> --command "node dist/index.mjs gateway --port 3000 --bind lan" -y
fly machine update <machine-id> --command "node dist/index.js gateway --port 3000 --bind lan" -y
# Or with memory increase
fly machine update <machine-id> --vm-memory 2048 --command "node dist/index.mjs gateway --port 3000 --bind lan" -y
fly machine update <machine-id> --vm-memory 2048 --command "node dist/index.js gateway --port 3000 --bind lan" -y
```
**Note:** After `fly deploy`, the machine command may reset to what's in `fly.toml`. If you made manual changes, re-apply them after deploy.
@@ -381,6 +393,7 @@ fly ips allocate-v6 --private -a my-openclaw
```
After this, `fly ips list` should show only a `private` type IP:
```
VERSION IP TYPE REGION
v6 fdaa:x:x:x:x::x private global
@@ -391,6 +404,7 @@ v6 fdaa:x:x:x:x::x private global
Since there's no public URL, use one of these methods:
**Option 1: Local proxy (simplest)**
```bash
# Forward local port 3000 to the app
fly proxy 3000:3000 -a my-openclaw
@@ -399,6 +413,7 @@ fly proxy 3000:3000 -a my-openclaw
```
**Option 2: WireGuard VPN**
```bash
# Create WireGuard config (one-time)
fly wireguard create
@@ -408,6 +423,7 @@ fly wireguard create
```
**Option 3: SSH only**
```bash
fly ssh console -a my-openclaw
```
@@ -421,6 +437,7 @@ If you need webhook callbacks (Twilio, Telnyx, etc.) without public exposure:
3. **Outbound-only** - Some providers (Twilio) work fine for outbound calls without webhooks
Example voice-call config with ngrok:
```json
{
"plugins": {
@@ -441,12 +458,12 @@ The ngrok tunnel runs inside the container and provides a public webhook URL wit
### Security benefits
| Aspect | Public | Private |
|--------|--------|---------|
| Internet scanners | Discoverable | Hidden |
| Direct attacks | Possible | Blocked |
| Control UI access | Browser | Proxy/VPN |
| Webhook delivery | Direct | Via tunnel |
| Aspect | Public | Private |
| ----------------- | ------------ | ---------- |
| Internet scanners | Discoverable | Hidden |
| Direct attacks | Possible | Blocked |
| Control UI access | Browser | Proxy/VPN |
| Webhook delivery | Direct | Via tunnel |
## Notes
@@ -459,6 +476,7 @@ The ngrok tunnel runs inside the container and provides a public webhook URL wit
## Cost
With the recommended config (`shared-cpu-2x`, 2GB RAM):
- ~$10-15/month depending on usage
- Free tier includes some allowance

View File

@@ -1,5 +1,5 @@
---
summary: "Run OpenClaw Gateway 24/7 on a GCP Compute Engine VM (Docker) with durable state"
summary: 'Run OpenClaw Gateway 24/7 on a GCP Compute Engine VM (Docker) with durable state'
read_when:
- You want OpenClaw running 24/7 on GCP
- You want a production-grade, always-on Gateway on your own VM
@@ -25,6 +25,7 @@ Pricing varies by machine type and region; pick the smallest VM that fits your w
- Access the Control UI from your laptop via an SSH tunnel
The Gateway can be accessed via:
- SSH port forwarding from your laptop
- Direct port exposure if you manage firewalling and tokens yourself
@@ -36,14 +37,14 @@ For the generic Docker flow, see [Docker](/install/docker).
## Quick path (experienced operators)
1) Create GCP project + enable Compute Engine API
2) Create Compute Engine VM (e2-small, Debian 12, 20GB)
3) SSH into the VM
4) Install Docker
5) Clone OpenClaw repository
6) Create persistent host directories
7) Configure `.env` and `docker-compose.yml`
8) Bake required binaries, build, and launch
1. Create GCP project + enable Compute Engine API
2. Create Compute Engine VM (e2-small, Debian 12, 20GB)
3. SSH into the VM
4. Install Docker
5. Clone OpenClaw repository
6. Create persistent host directories
7. Configure `.env` and `docker-compose.yml`
8. Bake required binaries, build, and launch
---
@@ -112,9 +113,9 @@ gcloud services enable compute.googleapis.com
**Machine types:**
| Type | Specs | Cost | Notes |
|------|-------|------|-------|
| e2-small | 2 vCPU, 2GB RAM | ~$12/mo | Recommended |
| Type | Specs | Cost | Notes |
| -------- | ------------------------ | ------------------ | ------------------ |
| e2-small | 2 vCPU, 2GB RAM | ~$12/mo | Recommended |
| e2-micro | 2 vCPU (shared), 1GB RAM | Free tier eligible | May OOM under load |
**CLI:**
@@ -263,20 +264,20 @@ services:
ports:
# Recommended: keep the Gateway loopback-only on the VM; access via SSH tunnel.
# To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly.
- "127.0.0.1:${OPENCLAW_GATEWAY_PORT}:18789"
- '127.0.0.1:${OPENCLAW_GATEWAY_PORT}:18789'
# Optional: only if you run iOS/Android nodes against this VM and need Canvas host.
# If you expose this publicly, read /gateway/security and firewall accordingly.
# - "18793:18793"
command:
[
"node",
"dist/index.mjs",
"gateway",
"--bind",
"${OPENCLAW_GATEWAY_BIND}",
"--port",
"${OPENCLAW_GATEWAY_PORT}"
'node',
'dist/index.js',
'gateway',
'--bind',
'${OPENCLAW_GATEWAY_BIND}',
'--port',
'${OPENCLAW_GATEWAY_PORT}',
]
```
@@ -290,6 +291,7 @@ Anything installed at runtime will be lost on restart.
All external binaries required by skills must be installed at image build time.
The examples below show three common binaries only:
- `gog` for Gmail access
- `goplaces` for Google Places
- `wacli` for WhatsApp
@@ -298,6 +300,7 @@ These are examples, not a complete list.
You may install as many binaries as needed using the same pattern.
If you add new skills later that depend on additional binaries, you must:
1. Update the Dockerfile
2. Rebuild the image
3. Restart the containers
@@ -338,7 +341,7 @@ RUN pnpm ui:build
ENV NODE_ENV=production
CMD ["node","dist/index.mjs"]
CMD ["node","dist/index.js"]
```
---
@@ -403,18 +406,18 @@ Paste your gateway token.
OpenClaw runs in Docker, but Docker is not the source of truth.
All long-lived state must survive restarts, rebuilds, and reboots.
| Component | Location | Persistence mechanism | Notes |
|---|---|---|---|
| Gateway config | `/home/node/.openclaw/` | Host volume mount | Includes `openclaw.json`, tokens |
| Model auth profiles | `/home/node/.openclaw/` | Host volume mount | OAuth tokens, API keys |
| Skill configs | `/home/node/.openclaw/skills/` | Host volume mount | Skill-level state |
| Agent workspace | `/home/node/.openclaw/workspace/` | Host volume mount | Code and agent artifacts |
| WhatsApp session | `/home/node/.openclaw/` | Host volume mount | Preserves QR login |
| Gmail keyring | `/home/node/.openclaw/` | Host volume + password | Requires `GOG_KEYRING_PASSWORD` |
| External binaries | `/usr/local/bin/` | Docker image | Must be baked at build time |
| Node runtime | Container filesystem | Docker image | Rebuilt every image build |
| OS packages | Container filesystem | Docker image | Do not install at runtime |
| Docker container | Ephemeral | Restartable | Safe to destroy |
| Component | Location | Persistence mechanism | Notes |
| ------------------- | --------------------------------- | ---------------------- | -------------------------------- |
| Gateway config | `/home/node/.openclaw/` | Host volume mount | Includes `openclaw.json`, tokens |
| Model auth profiles | `/home/node/.openclaw/` | Host volume mount | OAuth tokens, API keys |
| Skill configs | `/home/node/.openclaw/skills/` | Host volume mount | Skill-level state |
| Agent workspace | `/home/node/.openclaw/workspace/` | Host volume mount | Code and agent artifacts |
| WhatsApp session | `/home/node/.openclaw/` | Host volume mount | Preserves QR login |
| Gmail keyring | `/home/node/.openclaw/` | Host volume + password | Requires `GOG_KEYRING_PASSWORD` |
| External binaries | `/usr/local/bin/` | Docker image | Must be baked at build time |
| Node runtime | Container filesystem | Docker image | Rebuilt every image build |
| OS packages | Container filesystem | Docker image | Do not install at runtime |
| Docker container | Ephemeral | Restartable | Safe to destroy |
---
@@ -473,6 +476,7 @@ For personal use, your default user account works fine.
For automation or CI/CD pipelines, create a dedicated service account with minimal permissions:
1. Create a service account:
```bash
gcloud iam service-accounts create openclaw-deploy \
--display-name="OpenClaw Deployment"

View File

@@ -1,5 +1,5 @@
---
summary: "Run OpenClaw Gateway 24/7 on a cheap Hetzner VPS (Docker) with durable state and baked-in binaries"
summary: 'Run OpenClaw Gateway 24/7 on a cheap Hetzner VPS (Docker) with durable state and baked-in binaries'
read_when:
- You want OpenClaw running 24/7 on a cloud VPS (not your laptop)
- You want a production-grade, always-on Gateway on your own VPS
@@ -10,6 +10,7 @@ read_when:
# OpenClaw on Hetzner (Docker, Production VPS Guide)
## Goal
Run a persistent OpenClaw Gateway on a Hetzner VPS using Docker, with durable state, baked-in binaries, and safe restart behavior.
If you want “OpenClaw 24/7 for ~$5”, this is the simplest reliable setup.
@@ -24,6 +25,7 @@ Hetzner pricing changes; pick the smallest Debian/Ubuntu VPS and scale up if you
- Access the Control UI from your laptop via an SSH tunnel
The Gateway can be accessed via:
- SSH port forwarding from your laptop
- Direct port exposure if you manage firewalling and tokens yourself
@@ -35,29 +37,29 @@ For the generic Docker flow, see [Docker](/install/docker).
## Quick path (experienced operators)
1) Provision Hetzner VPS
2) Install Docker
3) Clone OpenClaw repository
4) Create persistent host directories
5) Configure `.env` and `docker-compose.yml`
6) Bake required binaries into the image
7) `docker compose up -d`
8) Verify persistence and Gateway access
1. Provision Hetzner VPS
2. Install Docker
3. Clone OpenClaw repository
4. Create persistent host directories
5. Configure `.env` and `docker-compose.yml`
6. Bake required binaries into the image
7. `docker compose up -d`
8. Verify persistence and Gateway access
---
## What you need
- Hetzner VPS with root access
- SSH access from your laptop
- Basic comfort with SSH + copy/paste
- ~20 minutes
- Docker and Docker Compose
- Model auth credentials
- Optional provider credentials
- WhatsApp QR
- Telegram bot token
- Gmail OAuth
- Hetzner VPS with root access
- SSH access from your laptop
- Basic comfort with SSH + copy/paste
- ~20 minutes
- Docker and Docker Compose
- Model auth credentials
- Optional provider credentials
- WhatsApp QR
- Telegram bot token
- Gmail OAuth
---
@@ -175,20 +177,20 @@ services:
ports:
# Recommended: keep the Gateway loopback-only on the VPS; access via SSH tunnel.
# To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly.
- "127.0.0.1:${OPENCLAW_GATEWAY_PORT}:18789"
- '127.0.0.1:${OPENCLAW_GATEWAY_PORT}:18789'
# Optional: only if you run iOS/Android nodes against this VPS and need Canvas host.
# If you expose this publicly, read /gateway/security and firewall accordingly.
# - "18793:18793"
command:
[
"node",
"dist/index.mjs",
"gateway",
"--bind",
"${OPENCLAW_GATEWAY_BIND}",
"--port",
"${OPENCLAW_GATEWAY_PORT}"
'node',
'dist/index.js',
'gateway',
'--bind',
'${OPENCLAW_GATEWAY_BIND}',
'--port',
'${OPENCLAW_GATEWAY_PORT}',
]
```
@@ -202,6 +204,7 @@ Anything installed at runtime will be lost on restart.
All external binaries required by skills must be installed at image build time.
The examples below show three common binaries only:
- `gog` for Gmail access
- `goplaces` for Google Places
- `wacli` for WhatsApp
@@ -210,6 +213,7 @@ These are examples, not a complete list.
You may install as many binaries as needed using the same pattern.
If you add new skills later that depend on additional binaries, you must:
1. Update the Dockerfile
2. Rebuild the image
3. Restart the containers
@@ -250,7 +254,7 @@ RUN pnpm ui:build
ENV NODE_ENV=production
CMD ["node","dist/index.mjs"]
CMD ["node","dist/index.js"]
```
---
@@ -311,15 +315,15 @@ Paste your gateway token.
OpenClaw runs in Docker, but Docker is not the source of truth.
All long-lived state must survive restarts, rebuilds, and reboots.
| Component | Location | Persistence mechanism | Notes |
|---|---|---|---|
| Gateway config | `/home/node/.openclaw/` | Host volume mount | Includes `openclaw.json`, tokens |
| Model auth profiles | `/home/node/.openclaw/` | Host volume mount | OAuth tokens, API keys |
| Skill configs | `/home/node/.openclaw/skills/` | Host volume mount | Skill-level state |
| Agent workspace | `/home/node/.openclaw/workspace/` | Host volume mount | Code and agent artifacts |
| WhatsApp session | `/home/node/.openclaw/` | Host volume mount | Preserves QR login |
| Gmail keyring | `/home/node/.openclaw/` | Host volume + password | Requires `GOG_KEYRING_PASSWORD` |
| External binaries | `/usr/local/bin/` | Docker image | Must be baked at build time |
| Node runtime | Container filesystem | Docker image | Rebuilt every image build |
| OS packages | Container filesystem | Docker image | Do not install at runtime |
| Docker container | Ephemeral | Restartable | Safe to destroy |
| Component | Location | Persistence mechanism | Notes |
| ------------------- | --------------------------------- | ---------------------- | -------------------------------- |
| Gateway config | `/home/node/.openclaw/` | Host volume mount | Includes `openclaw.json`, tokens |
| Model auth profiles | `/home/node/.openclaw/` | Host volume mount | OAuth tokens, API keys |
| Skill configs | `/home/node/.openclaw/skills/` | Host volume mount | Skill-level state |
| Agent workspace | `/home/node/.openclaw/workspace/` | Host volume mount | Code and agent artifacts |
| WhatsApp session | `/home/node/.openclaw/` | Host volume mount | Preserves QR login |
| Gmail keyring | `/home/node/.openclaw/` | Host volume + password | Requires `GOG_KEYRING_PASSWORD` |
| External binaries | `/usr/local/bin/` | Docker image | Must be baked at build time |
| Node runtime | Container filesystem | Docker image | Rebuilt every image build |
| OS packages | Container filesystem | Docker image | Do not install at runtime |
| Docker container | Ephemeral | Restartable | Safe to destroy |

View File

@@ -1,9 +1,10 @@
---
summary: "OpenClaw plugins/extensions: discovery, config, and safety"
summary: 'OpenClaw plugins/extensions: discovery, config, and safety'
read_when:
- Adding or modifying plugins/extensions
- Documenting plugin install or load rules
---
# Plugins (Extensions)
## Quick start (new to plugins?)
@@ -17,19 +18,19 @@ install).
Fast path:
1) See whats already loaded:
1. See whats already loaded:
```bash
openclaw plugins list
```
2) Install an official plugin (example: Voice Call):
2. Install an official plugin (example: Voice Call):
```bash
openclaw plugins install @openclaw/voice-call
```
3) Restart the Gateway, then configure under `plugins.entries.<id>.config`.
3. Restart the Gateway, then configure under `plugins.entries.<id>.config`.
See [Voice Call](/plugins/voice-call) for a concrete example plugin.
@@ -73,12 +74,13 @@ Plugins can access selected core helpers via `api.runtime`. For telephony TTS:
```ts
const result = await api.runtime.tts.textToSpeechTelephony({
text: "Hello from OpenClaw",
text: 'Hello from OpenClaw',
cfg: api.config,
});
```
Notes:
- Uses core `messages.tts` configuration (OpenAI or ElevenLabs).
- Returns PCM audio buffer + sample rate. Plugins must resample/encode for providers.
- Edge TTS is not supported for telephony.
@@ -87,18 +89,22 @@ Notes:
OpenClaw scans, in order:
1) Config paths
1. Config paths
- `plugins.load.paths` (file or directory)
2) Workspace extensions
2. Workspace extensions
- `<workspace>/.openclaw/extensions/*.ts`
- `<workspace>/.openclaw/extensions/*/index.ts`
3) Global extensions
3. Global extensions
- `~/.openclaw/extensions/*.ts`
- `~/.openclaw/extensions/*/index.ts`
4) Bundled extensions (shipped with OpenClaw, **disabled by default**)
4. Bundled extensions (shipped with OpenClaw, **disabled by default**)
- `<openclaw>/extensions/*`
Bundled plugins must be enabled explicitly via `plugins.entries.<id>.enabled`
@@ -164,6 +170,7 @@ Example:
OpenClaw can also merge **external channel catalogs** (for example, an MPM
registry export). Drop a JSON file at one of:
- `~/.openclaw/mpm/plugins.json`
- `~/.openclaw/mpm/catalog.json`
- `~/.openclaw/plugins/catalog.json`
@@ -188,17 +195,18 @@ configured id.
{
plugins: {
enabled: true,
allow: ["voice-call"],
deny: ["untrusted-plugin"],
load: { paths: ["~/Projects/oss/voice-call-extension"] },
allow: ['voice-call'],
deny: ['untrusted-plugin'],
load: { paths: ['~/Projects/oss/voice-call-extension'] },
entries: {
"voice-call": { enabled: true, config: { provider: "twilio" } }
}
}
'voice-call': { enabled: true, config: { provider: 'twilio' } },
},
},
}
```
Fields:
- `enabled`: master toggle (default: true)
- `allow`: allowlist (optional)
- `deny`: denylist (optional; deny wins)
@@ -208,6 +216,7 @@ Fields:
Config changes **require a gateway restart**.
Validation rules (strict):
- Unknown plugin ids in `entries`, `allow`, `deny`, or `slots` are **errors**.
- Unknown `channels.<id>` keys are **errors** unless a plugin manifest declares
the channel id.
@@ -224,9 +233,9 @@ Some plugin categories are **exclusive** (only one active at a time). Use
{
plugins: {
slots: {
memory: "memory-core" // or "none" to disable memory plugins
}
}
memory: 'memory-core', // or "none" to disable memory plugins
},
},
}
```
@@ -311,6 +320,7 @@ export default function register(api) {
```
Notes:
- Hook directories follow the normal hook structure (`HOOK.md` + `handler.ts`).
- Hook eligibility rules still apply (OS/bins/env/config requirements).
- Plugin-managed hooks show up in `openclaw hooks list` with `plugin:<id>`.
@@ -330,29 +340,29 @@ Example:
```ts
api.registerProvider({
id: "acme",
label: "AcmeAI",
id: 'acme',
label: 'AcmeAI',
auth: [
{
id: "oauth",
label: "OAuth",
kind: "oauth",
id: 'oauth',
label: 'OAuth',
kind: 'oauth',
run: async (ctx) => {
// Run OAuth flow and return auth profiles.
return {
profiles: [
{
profileId: "acme:default",
profileId: 'acme:default',
credential: {
type: "oauth",
provider: "acme",
access: "...",
refresh: "...",
type: 'oauth',
provider: 'acme',
access: '...',
refresh: '...',
expires: Date.now() + 3600 * 1000,
},
},
],
defaultModel: "acme/opus-1",
defaultModel: 'acme/opus-1',
};
},
},
@@ -361,6 +371,7 @@ api.registerProvider({
```
Notes:
- `run` receives a `ProviderAuthContext` with `prompter`, `runtime`,
`openUrl`, and `oauth.createVpsAwareHandlers` helpers.
- Return `configPatch` when you need to add default models or provider config.
@@ -374,23 +385,26 @@ validated by your channel plugin code.
```ts
const myChannel = {
id: "acmechat",
id: 'acmechat',
meta: {
id: "acmechat",
label: "AcmeChat",
selectionLabel: "AcmeChat (API)",
docsPath: "/channels/acmechat",
blurb: "demo channel plugin.",
aliases: ["acme"],
id: 'acmechat',
label: 'AcmeChat',
selectionLabel: 'AcmeChat (API)',
docsPath: '/channels/acmechat',
blurb: 'demo channel plugin.',
aliases: ['acme'],
},
capabilities: { chatTypes: ["direct"] },
capabilities: { chatTypes: ['direct'] },
config: {
listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
listAccountIds: (cfg) =>
Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
resolveAccount: (cfg, accountId) =>
(cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? { accountId }),
cfg.channels?.acmechat?.accounts?.[accountId ?? 'default'] ?? {
accountId,
},
},
outbound: {
deliveryMode: "direct",
deliveryMode: 'direct',
sendText: async () => ({ ok: true }),
},
};
@@ -401,6 +415,7 @@ export default function (api) {
```
Notes:
- Put config under `channels.<id>` (not `plugins.entries`).
- `meta.label` is used for labels in CLI/UI lists.
- `meta.aliases` adds alternate ids for normalization and CLI inputs.
@@ -412,27 +427,32 @@ Notes:
Use this when you want a **new chat surface** (a “messaging channel”), not a model provider.
Model provider docs live under `/providers/*`.
1) Pick an id + config shape
1. Pick an id + config shape
- All channel config lives under `channels.<id>`.
- Prefer `channels.<id>.accounts.<accountId>` for multiaccount setups.
2) Define the channel metadata
2. Define the channel metadata
- `meta.label`, `meta.selectionLabel`, `meta.docsPath`, `meta.blurb` control CLI/UI lists.
- `meta.docsPath` should point at a docs page like `/channels/<id>`.
- `meta.preferOver` lets a plugin replace another channel (auto-enable prefers it).
- `meta.detailLabel` and `meta.systemImage` are used by UIs for detail text/icons.
3) Implement the required adapters
3. Implement the required adapters
- `config.listAccountIds` + `config.resolveAccount`
- `capabilities` (chat types, media, threads, etc.)
- `outbound.deliveryMode` + `outbound.sendText` (for basic send)
4) Add optional adapters as needed
4. Add optional adapters as needed
- `setup` (wizard), `security` (DM policy), `status` (health/diagnostics)
- `gateway` (start/stop/login), `mentions`, `threading`, `streaming`
- `actions` (message actions), `commands` (native command behavior)
5) Register the channel in your plugin
5. Register the channel in your plugin
- `api.registerChannel({ plugin })`
Minimal config example:
@@ -442,10 +462,10 @@ Minimal config example:
channels: {
acmechat: {
accounts: {
default: { token: "ACME_TOKEN", enabled: true }
}
}
}
default: { token: 'ACME_TOKEN', enabled: true },
},
},
},
}
```
@@ -453,23 +473,26 @@ Minimal channel plugin (outboundonly):
```ts
const plugin = {
id: "acmechat",
id: 'acmechat',
meta: {
id: "acmechat",
label: "AcmeChat",
selectionLabel: "AcmeChat (API)",
docsPath: "/channels/acmechat",
blurb: "AcmeChat messaging channel.",
aliases: ["acme"],
id: 'acmechat',
label: 'AcmeChat',
selectionLabel: 'AcmeChat (API)',
docsPath: '/channels/acmechat',
blurb: 'AcmeChat messaging channel.',
aliases: ['acme'],
},
capabilities: { chatTypes: ["direct"] },
capabilities: { chatTypes: ['direct'] },
config: {
listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
listAccountIds: (cfg) =>
Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
resolveAccount: (cfg, accountId) =>
(cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? { accountId }),
cfg.channels?.acmechat?.accounts?.[accountId ?? 'default'] ?? {
accountId,
},
},
outbound: {
deliveryMode: "direct",
deliveryMode: 'direct',
sendText: async ({ text }) => {
// deliver `text` to your channel here
return { ok: true };
@@ -493,7 +516,7 @@ See the dedicated guide: [Plugin agent tools](/plugins/agent-tools).
```ts
export default function (api) {
api.registerGatewayMethod("myplugin.status", ({ respond }) => {
api.registerGatewayMethod('myplugin.status', ({ respond }) => {
respond(true, { ok: true });
});
}
@@ -503,11 +526,14 @@ export default function (api) {
```ts
export default function (api) {
api.registerCli(({ program }) => {
program.command("mycmd").action(() => {
console.log("Hello");
});
}, { commands: ["mycmd"] });
api.registerCli(
({ program }) => {
program.command('mycmd').action(() => {
console.log('Hello');
});
},
{ commands: ['mycmd'] },
);
}
```
@@ -520,8 +546,8 @@ that don't need LLM processing.
```ts
export default function (api) {
api.registerCommand({
name: "mystatus",
description: "Show plugin status",
name: 'mystatus',
description: 'Show plugin status',
handler: (ctx) => ({
text: `Plugin is running! Channel: ${ctx.channel}`,
}),
@@ -550,12 +576,12 @@ Example with authorization and arguments:
```ts
api.registerCommand({
name: "setmode",
description: "Set plugin mode",
name: 'setmode',
description: 'Set plugin mode',
acceptsArgs: true,
requireAuth: true,
handler: async (ctx) => {
const mode = ctx.args?.trim() || "default";
const mode = ctx.args?.trim() || 'default';
await saveMode(mode);
return { text: `Mode set to: ${mode}` };
},
@@ -563,6 +589,7 @@ api.registerCommand({
```
Notes:
- Plugin commands are processed **before** built-in commands and the AI agent
- Commands are registered globally and work across all channels
- Command names are case-insensitive (`/MyStatus` matches `/mystatus`)
@@ -575,9 +602,9 @@ Notes:
```ts
export default function (api) {
api.registerService({
id: "my-service",
start: () => api.logger.info("ready"),
stop: () => api.logger.info("bye"),
id: 'my-service',
start: () => api.logger.info('ready'),
stop: () => api.logger.info('bye'),
});
}
```
@@ -635,4 +662,4 @@ Plugins run in-process with the Gateway. Treat them as trusted code:
Plugins can (and should) ship tests:
- In-repo plugins can keep Vitest tests under `src/**` (example: `src/plugins/voice-call.plugin.test.ts`).
- Separately published plugins should run their own CI (lint/build/test) and validate `openclaw.extensions` points at the built entrypoint (`dist/index.mjs`).
- Separately published plugins should run their own CI (lint/build/test) and validate `openclaw.extensions` points at the built entrypoint (`dist/index.js`).