Merge remote-tracking branch 'upstream/main' into feat/gigachat
# Conflicts: # extensions/discord/src/accounts.ts # extensions/whatsapp/src/test-helpers.ts
This commit is contained in:
commit
a1182d1faa
@ -1,7 +1,7 @@
|
||||
.git
|
||||
.worktrees
|
||||
|
||||
# Sensitive files – docker-setup.sh writes .env with OPENCLAW_GATEWAY_TOKEN
|
||||
# Sensitive files – scripts/docker/setup.sh writes .env with OPENCLAW_GATEWAY_TOKEN
|
||||
# into the project root; keep it out of the build context.
|
||||
.env
|
||||
.env.*
|
||||
|
||||
3
.github/labeler.yml
vendored
3
.github/labeler.yml
vendored
@ -165,7 +165,10 @@
|
||||
- "Dockerfile.*"
|
||||
- "docker-compose.yml"
|
||||
- "docker-setup.sh"
|
||||
- "setup-podman.sh"
|
||||
- ".dockerignore"
|
||||
- "scripts/docker/setup.sh"
|
||||
- "scripts/podman/setup.sh"
|
||||
- "scripts/**/*docker*"
|
||||
- "scripts/**/Dockerfile*"
|
||||
- "scripts/sandbox-*.sh"
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -496,7 +496,9 @@ jobs:
|
||||
run: pnpm test
|
||||
|
||||
- name: Verify npm pack under Node 22
|
||||
run: pnpm release:check
|
||||
run: |
|
||||
node scripts/stage-bundled-plugin-runtime-deps.mjs
|
||||
node --import tsx scripts/release-check.ts
|
||||
|
||||
skills-python:
|
||||
needs: [docs-scope, changed-scope]
|
||||
|
||||
@ -118,6 +118,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Slack/startup: harden `@slack/bolt` import interop across current bundled runtime shapes so Slack monitors no longer crash with `App is not a constructor` after plugin-sdk bundling changes. (#45953) Thanks @merc1305.
|
||||
- Windows/gateway status: accept `schtasks` `Last Result` output as an alias for `Last Run Result`, so running scheduled-task installs no longer show `Runtime: unknown`. (#47844) Thanks @MoerAI.
|
||||
- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup. (#47601) Thanks @ngutman.
|
||||
- Gateway/WS handshake: raise the default pre-auth handshake timeout to 10 seconds and add `OPENCLAW_HANDSHAKE_TIMEOUT_MS` as a runtime override so busy local gateways stop dropping healthy CLI connections at 3 seconds. (#49262) Thanks @fuller-stack-dev.
|
||||
- Gateway/websocket pairing bypass for disabled auth: skip device-pairing enforcement for Control UI operator sessions when `gateway.auth.mode=none`, so reverse-proxied dashboards no longer get stuck on `pairing required` despite auth being explicitly disabled. (#47148) Thanks @ademczuk.
|
||||
- Control UI/model switching: preserve the selected provider prefix when switching models from the chat dropdown, so multi-provider setups no longer send `anthropic/gpt-5.2`-style mismatches when the user picked `openai/gpt-5.2`. (#47581) Thanks @chrishham.
|
||||
- Control UI/storage: scope persisted settings keys by gateway base path, with migration from the legacy shared key, so multiple gateways under one domain stop overwriting each other's dashboard preferences. (#47932) Thanks @bobBot-claw.
|
||||
@ -164,6 +165,7 @@ Docs: https://docs.openclaw.ai
|
||||
- WhatsApp/active-listener: pin the active listener registry to a `globalThis` singleton so split WhatsApp bundle chunks share one listener map and outbound sends stop missing the registered session. (#47433) Thanks @clawdia67.
|
||||
- Plugins/WhatsApp: share split-load singleton state for plugin command registration and active WhatsApp listeners so duplicate module graphs no longer lose native plugin commands or outbound listener state. (#50418) Thanks @huntharo.
|
||||
- Onboarding/custom providers: keep Azure AI Foundry `*.services.ai.azure.com` custom endpoints on the selected compatibility path instead of forcing Responses, so chat-completions Foundry models still work after setup. Fixes #50528. (#50535) Thanks @obviyus.
|
||||
- Plugins/update: let `openclaw plugins update <npm-spec>` target tracked npm installs by dist-tag or exact version, and preserve the recorded npm spec for later id-based updates. (#49998) Thanks @huntharo.
|
||||
|
||||
### Breaking
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ Model note: while many providers/models are supported, for the best experience a
|
||||
|
||||
## Install (recommended)
|
||||
|
||||
Runtime: **Node ≥22**.
|
||||
Runtime: **Node 24 (recommended) or Node 22.16+**.
|
||||
|
||||
```bash
|
||||
npm install -g openclaw@latest
|
||||
@ -62,7 +62,7 @@ OpenClaw Onboard installs the Gateway daemon (launchd/systemd user service) so i
|
||||
|
||||
## Quick start (TL;DR)
|
||||
|
||||
Runtime: **Node ≥22**.
|
||||
Runtime: **Node 24 (recommended) or Node 22.16+**.
|
||||
|
||||
Full beginner guide (auth, pairing, channels): [Getting started](https://docs.openclaw.ai/start/getting-started)
|
||||
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
### Fixes
|
||||
|
||||
- Gateway/session history: return `404` for unknown session history lookups, unsubscribe session lifecycle listeners during shutdown, add coverage for the new transcript and lifecycle helpers, and tighten session history plus live transcript tests so the Control UI session surfaces stay stable under restart and follow mode.
|
||||
@ -16,7 +16,7 @@ services:
|
||||
## Uncomment the lines below to enable sandbox isolation
|
||||
## (agents.defaults.sandbox). Requires Docker CLI in the image
|
||||
## (build with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1) or use
|
||||
## docker-setup.sh with OPENCLAW_SANDBOX=1 for automated setup.
|
||||
## scripts/docker/setup.sh with OPENCLAW_SANDBOX=1 for automated setup.
|
||||
## Set DOCKER_GID to the host's docker group GID (run: stat -c '%g' /var/run/docker.sock).
|
||||
# - /var/run/docker.sock:/var/run/docker.sock
|
||||
# group_add:
|
||||
|
||||
612
docker-setup.sh
612
docker-setup.sh
@ -2,615 +2,11 @@
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
COMPOSE_FILE="$ROOT_DIR/docker-compose.yml"
|
||||
EXTRA_COMPOSE_FILE="$ROOT_DIR/docker-compose.extra.yml"
|
||||
IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}"
|
||||
EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}"
|
||||
HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}"
|
||||
RAW_SANDBOX_SETTING="${OPENCLAW_SANDBOX:-}"
|
||||
SANDBOX_ENABLED=""
|
||||
DOCKER_SOCKET_PATH="${OPENCLAW_DOCKER_SOCKET:-}"
|
||||
TIMEZONE="${OPENCLAW_TZ:-}"
|
||||
SCRIPT_PATH="$ROOT_DIR/scripts/docker/setup.sh"
|
||||
|
||||
fail() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Missing dependency: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_truthy_value() {
|
||||
local raw="${1:-}"
|
||||
raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
||||
case "$raw" in
|
||||
1 | true | yes | on) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
read_config_gateway_token() {
|
||||
local config_path="$OPENCLAW_CONFIG_DIR/openclaw.json"
|
||||
if [[ ! -f "$config_path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 - "$config_path" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
except Exception:
|
||||
raise SystemExit(0)
|
||||
|
||||
gateway = cfg.get("gateway")
|
||||
if not isinstance(gateway, dict):
|
||||
raise SystemExit(0)
|
||||
auth = gateway.get("auth")
|
||||
if not isinstance(auth, dict):
|
||||
raise SystemExit(0)
|
||||
token = auth.get("token")
|
||||
if isinstance(token, str):
|
||||
token = token.strip()
|
||||
if token:
|
||||
print(token)
|
||||
PY
|
||||
return 0
|
||||
fi
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
node - "$config_path" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const configPath = process.argv[2];
|
||||
try {
|
||||
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
const token = cfg?.gateway?.auth?.token;
|
||||
if (typeof token === "string" && token.trim().length > 0) {
|
||||
process.stdout.write(token.trim());
|
||||
}
|
||||
} catch {
|
||||
// Keep docker-setup resilient when config parsing fails.
|
||||
}
|
||||
NODE
|
||||
fi
|
||||
}
|
||||
|
||||
read_env_gateway_token() {
|
||||
local env_path="$1"
|
||||
local line=""
|
||||
local token=""
|
||||
if [[ ! -f "$env_path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%$'\r'}"
|
||||
if [[ "$line" == OPENCLAW_GATEWAY_TOKEN=* ]]; then
|
||||
token="${line#OPENCLAW_GATEWAY_TOKEN=}"
|
||||
fi
|
||||
done <"$env_path"
|
||||
if [[ -n "$token" ]]; then
|
||||
printf '%s' "$token"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_control_ui_allowed_origins() {
|
||||
if [[ "${OPENCLAW_GATEWAY_BIND}" == "loopback" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local allowed_origin_json
|
||||
local current_allowed_origins
|
||||
allowed_origin_json="$(printf '["http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT")"
|
||||
current_allowed_origins="$(
|
||||
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
||||
config get gateway.controlUi.allowedOrigins 2>/dev/null || true
|
||||
)"
|
||||
current_allowed_origins="${current_allowed_origins//$'\r'/}"
|
||||
|
||||
if [[ -n "$current_allowed_origins" && "$current_allowed_origins" != "null" && "$current_allowed_origins" != "[]" ]]; then
|
||||
echo "Control UI allowlist already configured; leaving gateway.controlUi.allowedOrigins unchanged."
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
||||
config set gateway.controlUi.allowedOrigins "$allowed_origin_json" --strict-json >/dev/null
|
||||
echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind."
|
||||
}
|
||||
|
||||
sync_gateway_mode_and_bind() {
|
||||
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
||||
config set gateway.mode local >/dev/null
|
||||
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
||||
config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null
|
||||
echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup."
|
||||
}
|
||||
|
||||
contains_disallowed_chars() {
|
||||
local value="$1"
|
||||
[[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]]
|
||||
}
|
||||
|
||||
is_valid_timezone() {
|
||||
local value="$1"
|
||||
[[ -e "/usr/share/zoneinfo/$value" && ! -d "/usr/share/zoneinfo/$value" ]]
|
||||
}
|
||||
|
||||
validate_mount_path_value() {
|
||||
local label="$1"
|
||||
local value="$2"
|
||||
if [[ -z "$value" ]]; then
|
||||
fail "$label cannot be empty."
|
||||
fi
|
||||
if contains_disallowed_chars "$value"; then
|
||||
fail "$label contains unsupported control characters."
|
||||
fi
|
||||
if [[ "$value" =~ [[:space:]] ]]; then
|
||||
fail "$label cannot contain whitespace."
|
||||
fi
|
||||
}
|
||||
|
||||
validate_named_volume() {
|
||||
local value="$1"
|
||||
if [[ ! "$value" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then
|
||||
fail "OPENCLAW_HOME_VOLUME must match [A-Za-z0-9][A-Za-z0-9_.-]* when using a named volume."
|
||||
fi
|
||||
}
|
||||
|
||||
validate_mount_spec() {
|
||||
local mount="$1"
|
||||
if contains_disallowed_chars "$mount"; then
|
||||
fail "OPENCLAW_EXTRA_MOUNTS entries cannot contain control characters."
|
||||
fi
|
||||
# Keep mount specs strict to avoid YAML structure injection.
|
||||
# Expected format: source:target[:options]
|
||||
if [[ ! "$mount" =~ ^[^[:space:],:]+:[^[:space:],:]+(:[^[:space:],:]+)?$ ]]; then
|
||||
fail "Invalid mount format '$mount'. Expected source:target[:options] without spaces."
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd docker
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "Docker Compose not available (try: docker compose version)" >&2
|
||||
if [[ ! -f "$SCRIPT_PATH" ]]; then
|
||||
echo "Docker setup script not found at $SCRIPT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$DOCKER_SOCKET_PATH" && "${DOCKER_HOST:-}" == unix://* ]]; then
|
||||
DOCKER_SOCKET_PATH="${DOCKER_HOST#unix://}"
|
||||
fi
|
||||
if [[ -z "$DOCKER_SOCKET_PATH" ]]; then
|
||||
DOCKER_SOCKET_PATH="/var/run/docker.sock"
|
||||
fi
|
||||
if is_truthy_value "$RAW_SANDBOX_SETTING"; then
|
||||
SANDBOX_ENABLED="1"
|
||||
fi
|
||||
|
||||
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
|
||||
OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
|
||||
|
||||
validate_mount_path_value "OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR"
|
||||
validate_mount_path_value "OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_WORKSPACE_DIR"
|
||||
if [[ -n "$HOME_VOLUME_NAME" ]]; then
|
||||
if [[ "$HOME_VOLUME_NAME" == *"/"* ]]; then
|
||||
validate_mount_path_value "OPENCLAW_HOME_VOLUME" "$HOME_VOLUME_NAME"
|
||||
else
|
||||
validate_named_volume "$HOME_VOLUME_NAME"
|
||||
fi
|
||||
fi
|
||||
if contains_disallowed_chars "$EXTRA_MOUNTS"; then
|
||||
fail "OPENCLAW_EXTRA_MOUNTS cannot contain control characters."
|
||||
fi
|
||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||
validate_mount_path_value "OPENCLAW_DOCKER_SOCKET" "$DOCKER_SOCKET_PATH"
|
||||
fi
|
||||
if [[ -n "$TIMEZONE" ]]; then
|
||||
if contains_disallowed_chars "$TIMEZONE"; then
|
||||
fail "OPENCLAW_TZ contains unsupported control characters."
|
||||
fi
|
||||
if [[ ! "$TIMEZONE" =~ ^[A-Za-z0-9/_+\-]+$ ]]; then
|
||||
fail "OPENCLAW_TZ must be a valid IANA timezone string (e.g. Asia/Shanghai)."
|
||||
fi
|
||||
if ! is_valid_timezone "$TIMEZONE"; then
|
||||
fail "OPENCLAW_TZ must match a timezone in /usr/share/zoneinfo (e.g. Asia/Shanghai)."
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$OPENCLAW_CONFIG_DIR"
|
||||
mkdir -p "$OPENCLAW_WORKSPACE_DIR"
|
||||
# Seed directory tree eagerly so bind mounts work even on Docker Desktop/Windows
|
||||
# where the container (even as root) cannot create new host subdirectories.
|
||||
mkdir -p "$OPENCLAW_CONFIG_DIR/identity"
|
||||
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/agent"
|
||||
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/sessions"
|
||||
|
||||
export OPENCLAW_CONFIG_DIR
|
||||
export OPENCLAW_WORKSPACE_DIR
|
||||
export OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
|
||||
export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}"
|
||||
export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}"
|
||||
export OPENCLAW_IMAGE="$IMAGE_NAME"
|
||||
export OPENCLAW_DOCKER_APT_PACKAGES="${OPENCLAW_DOCKER_APT_PACKAGES:-}"
|
||||
export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}"
|
||||
export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS"
|
||||
export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME"
|
||||
export OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}"
|
||||
export OPENCLAW_SANDBOX="$SANDBOX_ENABLED"
|
||||
export OPENCLAW_DOCKER_SOCKET="$DOCKER_SOCKET_PATH"
|
||||
export OPENCLAW_TZ="$TIMEZONE"
|
||||
|
||||
# Detect Docker socket GID for sandbox group_add.
|
||||
DOCKER_GID=""
|
||||
if [[ -n "$SANDBOX_ENABLED" && -S "$DOCKER_SOCKET_PATH" ]]; then
|
||||
DOCKER_GID="$(stat -c '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || stat -f '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || echo "")"
|
||||
fi
|
||||
export DOCKER_GID
|
||||
|
||||
if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then
|
||||
EXISTING_CONFIG_TOKEN="$(read_config_gateway_token || true)"
|
||||
if [[ -n "$EXISTING_CONFIG_TOKEN" ]]; then
|
||||
OPENCLAW_GATEWAY_TOKEN="$EXISTING_CONFIG_TOKEN"
|
||||
echo "Reusing gateway token from $OPENCLAW_CONFIG_DIR/openclaw.json"
|
||||
else
|
||||
DOTENV_GATEWAY_TOKEN="$(read_env_gateway_token "$ROOT_DIR/.env" || true)"
|
||||
if [[ -n "$DOTENV_GATEWAY_TOKEN" ]]; then
|
||||
OPENCLAW_GATEWAY_TOKEN="$DOTENV_GATEWAY_TOKEN"
|
||||
echo "Reusing gateway token from $ROOT_DIR/.env"
|
||||
elif command -v openssl >/dev/null 2>&1; then
|
||||
OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)"
|
||||
else
|
||||
OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY'
|
||||
import secrets
|
||||
print(secrets.token_hex(32))
|
||||
PY
|
||||
)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
export OPENCLAW_GATEWAY_TOKEN
|
||||
|
||||
COMPOSE_FILES=("$COMPOSE_FILE")
|
||||
COMPOSE_ARGS=()
|
||||
|
||||
write_extra_compose() {
|
||||
local home_volume="$1"
|
||||
shift
|
||||
local mount
|
||||
local gateway_home_mount
|
||||
local gateway_config_mount
|
||||
local gateway_workspace_mount
|
||||
|
||||
cat >"$EXTRA_COMPOSE_FILE" <<'YAML'
|
||||
services:
|
||||
openclaw-gateway:
|
||||
volumes:
|
||||
YAML
|
||||
|
||||
if [[ -n "$home_volume" ]]; then
|
||||
gateway_home_mount="${home_volume}:/home/node"
|
||||
gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw"
|
||||
gateway_workspace_mount="${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace"
|
||||
validate_mount_spec "$gateway_home_mount"
|
||||
validate_mount_spec "$gateway_config_mount"
|
||||
validate_mount_spec "$gateway_workspace_mount"
|
||||
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||
fi
|
||||
|
||||
for mount in "$@"; do
|
||||
validate_mount_spec "$mount"
|
||||
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
||||
done
|
||||
|
||||
cat >>"$EXTRA_COMPOSE_FILE" <<'YAML'
|
||||
openclaw-cli:
|
||||
volumes:
|
||||
YAML
|
||||
|
||||
if [[ -n "$home_volume" ]]; then
|
||||
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||
fi
|
||||
|
||||
for mount in "$@"; do
|
||||
validate_mount_spec "$mount"
|
||||
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
||||
done
|
||||
|
||||
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
|
||||
validate_named_volume "$home_volume"
|
||||
cat >>"$EXTRA_COMPOSE_FILE" <<YAML
|
||||
volumes:
|
||||
${home_volume}:
|
||||
YAML
|
||||
fi
|
||||
}
|
||||
|
||||
# When sandbox is requested, ensure Docker CLI build arg is set for local builds.
|
||||
# Docker socket mount is deferred until sandbox prerequisites are verified.
|
||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||
if [[ -z "${OPENCLAW_INSTALL_DOCKER_CLI:-}" ]]; then
|
||||
export OPENCLAW_INSTALL_DOCKER_CLI=1
|
||||
fi
|
||||
fi
|
||||
|
||||
VALID_MOUNTS=()
|
||||
if [[ -n "$EXTRA_MOUNTS" ]]; then
|
||||
IFS=',' read -r -a mounts <<<"$EXTRA_MOUNTS"
|
||||
for mount in "${mounts[@]}"; do
|
||||
mount="${mount#"${mount%%[![:space:]]*}"}"
|
||||
mount="${mount%"${mount##*[![:space:]]}"}"
|
||||
if [[ -n "$mount" ]]; then
|
||||
VALID_MOUNTS+=("$mount")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -n "$HOME_VOLUME_NAME" || ${#VALID_MOUNTS[@]} -gt 0 ]]; then
|
||||
# Bash 3.2 + nounset treats "${array[@]}" on an empty array as unbound.
|
||||
if [[ ${#VALID_MOUNTS[@]} -gt 0 ]]; then
|
||||
write_extra_compose "$HOME_VOLUME_NAME" "${VALID_MOUNTS[@]}"
|
||||
else
|
||||
write_extra_compose "$HOME_VOLUME_NAME"
|
||||
fi
|
||||
COMPOSE_FILES+=("$EXTRA_COMPOSE_FILE")
|
||||
fi
|
||||
for compose_file in "${COMPOSE_FILES[@]}"; do
|
||||
COMPOSE_ARGS+=("-f" "$compose_file")
|
||||
done
|
||||
# Keep a base compose arg set without sandbox overlay so rollback paths can
|
||||
# force a known-safe gateway service definition (no docker.sock mount).
|
||||
BASE_COMPOSE_ARGS=("${COMPOSE_ARGS[@]}")
|
||||
COMPOSE_HINT="docker compose"
|
||||
for compose_file in "${COMPOSE_FILES[@]}"; do
|
||||
COMPOSE_HINT+=" -f ${compose_file}"
|
||||
done
|
||||
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
upsert_env() {
|
||||
local file="$1"
|
||||
shift
|
||||
local -a keys=("$@")
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
# Use a delimited string instead of an associative array so the script
|
||||
# works with Bash 3.2 (macOS default) which lacks `declare -A`.
|
||||
local seen=" "
|
||||
|
||||
if [[ -f "$file" ]]; then
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
local key="${line%%=*}"
|
||||
local replaced=false
|
||||
for k in "${keys[@]}"; do
|
||||
if [[ "$key" == "$k" ]]; then
|
||||
printf '%s=%s\n' "$k" "${!k-}" >>"$tmp"
|
||||
seen="$seen$k "
|
||||
replaced=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$replaced" == false ]]; then
|
||||
printf '%s\n' "$line" >>"$tmp"
|
||||
fi
|
||||
done <"$file"
|
||||
fi
|
||||
|
||||
for k in "${keys[@]}"; do
|
||||
if [[ "$seen" != *" $k "* ]]; then
|
||||
printf '%s=%s\n' "$k" "${!k-}" >>"$tmp"
|
||||
fi
|
||||
done
|
||||
|
||||
mv "$tmp" "$file"
|
||||
}
|
||||
|
||||
upsert_env "$ENV_FILE" \
|
||||
OPENCLAW_CONFIG_DIR \
|
||||
OPENCLAW_WORKSPACE_DIR \
|
||||
OPENCLAW_GATEWAY_PORT \
|
||||
OPENCLAW_BRIDGE_PORT \
|
||||
OPENCLAW_GATEWAY_BIND \
|
||||
OPENCLAW_GATEWAY_TOKEN \
|
||||
OPENCLAW_IMAGE \
|
||||
OPENCLAW_EXTRA_MOUNTS \
|
||||
OPENCLAW_HOME_VOLUME \
|
||||
OPENCLAW_DOCKER_APT_PACKAGES \
|
||||
OPENCLAW_EXTENSIONS \
|
||||
OPENCLAW_SANDBOX \
|
||||
OPENCLAW_DOCKER_SOCKET \
|
||||
DOCKER_GID \
|
||||
OPENCLAW_INSTALL_DOCKER_CLI \
|
||||
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS \
|
||||
OPENCLAW_TZ
|
||||
|
||||
if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then
|
||||
echo "==> Building Docker image: $IMAGE_NAME"
|
||||
docker build \
|
||||
--build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}" \
|
||||
--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \
|
||||
--build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \
|
||||
-t "$IMAGE_NAME" \
|
||||
-f "$ROOT_DIR/Dockerfile" \
|
||||
"$ROOT_DIR"
|
||||
else
|
||||
echo "==> Pulling Docker image: $IMAGE_NAME"
|
||||
if ! docker pull "$IMAGE_NAME"; then
|
||||
echo "ERROR: Failed to pull image $IMAGE_NAME. Please check the image name and your access permissions." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure bind-mounted data directories are writable by the container's `node`
|
||||
# user (uid 1000). Host-created dirs inherit the host user's uid which may
|
||||
# differ, causing EACCES when the container tries to mkdir/write.
|
||||
# Running a brief root container to chown is the portable Docker idiom --
|
||||
# it works regardless of the host uid and doesn't require host-side root.
|
||||
echo ""
|
||||
echo "==> Fixing data-directory permissions"
|
||||
# Use -xdev to restrict chown to the config-dir mount only — without it,
|
||||
# the recursive chown would cross into the workspace bind mount and rewrite
|
||||
# ownership of all user project files on Linux hosts.
|
||||
# After fixing the config dir, only the OpenClaw metadata subdirectory
|
||||
# (.openclaw/) inside the workspace gets chowned, not the user's project files.
|
||||
docker compose "${COMPOSE_ARGS[@]}" run --rm --user root --entrypoint sh openclaw-cli -c \
|
||||
'find /home/node/.openclaw -xdev -exec chown node:node {} +; \
|
||||
[ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true'
|
||||
|
||||
echo ""
|
||||
echo "==> Onboarding (interactive)"
|
||||
echo "Docker setup pins Gateway mode to local."
|
||||
echo "Gateway runtime bind comes from OPENCLAW_GATEWAY_BIND (default: lan)."
|
||||
echo "Current runtime bind: $OPENCLAW_GATEWAY_BIND"
|
||||
echo "Gateway token: $OPENCLAW_GATEWAY_TOKEN"
|
||||
echo "Tailscale exposure: Off (use host-level tailnet/Tailscale setup separately)."
|
||||
echo "Install Gateway daemon: No (managed by Docker Compose)"
|
||||
echo ""
|
||||
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli onboard --mode local --no-install-daemon
|
||||
|
||||
echo ""
|
||||
echo "==> Docker gateway defaults"
|
||||
sync_gateway_mode_and_bind
|
||||
|
||||
echo ""
|
||||
echo "==> Control UI origin allowlist"
|
||||
ensure_control_ui_allowed_origins
|
||||
|
||||
echo ""
|
||||
echo "==> Provider setup (optional)"
|
||||
echo "WhatsApp (QR):"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels login"
|
||||
echo "Telegram (bot token):"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel telegram --token <token>"
|
||||
echo "Discord (bot token):"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel discord --token <token>"
|
||||
echo "Docs: https://docs.openclaw.ai/channels"
|
||||
|
||||
echo ""
|
||||
echo "==> Starting gateway"
|
||||
docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway
|
||||
|
||||
# --- Sandbox setup (opt-in via OPENCLAW_SANDBOX=1) ---
|
||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||
echo ""
|
||||
echo "==> Sandbox setup"
|
||||
|
||||
# Build sandbox image if Dockerfile.sandbox exists.
|
||||
if [[ -f "$ROOT_DIR/Dockerfile.sandbox" ]]; then
|
||||
echo "Building sandbox image: openclaw-sandbox:bookworm-slim"
|
||||
docker build \
|
||||
-t "openclaw-sandbox:bookworm-slim" \
|
||||
-f "$ROOT_DIR/Dockerfile.sandbox" \
|
||||
"$ROOT_DIR"
|
||||
else
|
||||
echo "WARNING: Dockerfile.sandbox not found in $ROOT_DIR" >&2
|
||||
echo " Sandbox config will be applied but no sandbox image will be built." >&2
|
||||
echo " Agent exec may fail if the configured sandbox image does not exist." >&2
|
||||
fi
|
||||
|
||||
# Defense-in-depth: verify Docker CLI in the running image before enabling
|
||||
# sandbox. This avoids claiming sandbox is enabled when the image cannot
|
||||
# launch sandbox containers.
|
||||
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --entrypoint docker openclaw-gateway --version >/dev/null 2>&1; then
|
||||
echo "WARNING: Docker CLI not found inside the container image." >&2
|
||||
echo " Sandbox requires Docker CLI. Rebuild with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1" >&2
|
||||
echo " or use a local build (OPENCLAW_IMAGE=openclaw:local). Skipping sandbox setup." >&2
|
||||
SANDBOX_ENABLED=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Apply sandbox config only if prerequisites are met.
|
||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||
# Mount Docker socket via a dedicated compose overlay. This overlay is
|
||||
# created only after sandbox prerequisites pass, so the socket is never
|
||||
# exposed when sandbox cannot actually run.
|
||||
if [[ -S "$DOCKER_SOCKET_PATH" ]]; then
|
||||
SANDBOX_COMPOSE_FILE="$ROOT_DIR/docker-compose.sandbox.yml"
|
||||
cat >"$SANDBOX_COMPOSE_FILE" <<YAML
|
||||
services:
|
||||
openclaw-gateway:
|
||||
volumes:
|
||||
- ${DOCKER_SOCKET_PATH}:/var/run/docker.sock
|
||||
YAML
|
||||
if [[ -n "${DOCKER_GID:-}" ]]; then
|
||||
cat >>"$SANDBOX_COMPOSE_FILE" <<YAML
|
||||
group_add:
|
||||
- "${DOCKER_GID}"
|
||||
YAML
|
||||
fi
|
||||
COMPOSE_ARGS+=("-f" "$SANDBOX_COMPOSE_FILE")
|
||||
echo "==> Sandbox: added Docker socket mount"
|
||||
else
|
||||
echo "WARNING: OPENCLAW_SANDBOX enabled but Docker socket not found at $DOCKER_SOCKET_PATH." >&2
|
||||
echo " Sandbox requires Docker socket access. Skipping sandbox setup." >&2
|
||||
SANDBOX_ENABLED=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||
# Enable sandbox in OpenClaw config.
|
||||
sandbox_config_ok=true
|
||||
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
||||
config set agents.defaults.sandbox.mode "non-main" >/dev/null; then
|
||||
echo "WARNING: Failed to set agents.defaults.sandbox.mode" >&2
|
||||
sandbox_config_ok=false
|
||||
fi
|
||||
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
||||
config set agents.defaults.sandbox.scope "agent" >/dev/null; then
|
||||
echo "WARNING: Failed to set agents.defaults.sandbox.scope" >&2
|
||||
sandbox_config_ok=false
|
||||
fi
|
||||
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
||||
config set agents.defaults.sandbox.workspaceAccess "none" >/dev/null; then
|
||||
echo "WARNING: Failed to set agents.defaults.sandbox.workspaceAccess" >&2
|
||||
sandbox_config_ok=false
|
||||
fi
|
||||
|
||||
if [[ "$sandbox_config_ok" == true ]]; then
|
||||
echo "Sandbox enabled: mode=non-main, scope=agent, workspaceAccess=none"
|
||||
echo "Docs: https://docs.openclaw.ai/gateway/sandboxing"
|
||||
# Restart gateway with sandbox compose overlay to pick up socket mount + config.
|
||||
docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway
|
||||
else
|
||||
echo "WARNING: Sandbox config was partially applied. Check errors above." >&2
|
||||
echo " Skipping gateway restart to avoid exposing Docker socket without a full sandbox policy." >&2
|
||||
if ! docker compose "${BASE_COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
||||
config set agents.defaults.sandbox.mode "off" >/dev/null; then
|
||||
echo "WARNING: Failed to roll back agents.defaults.sandbox.mode to off" >&2
|
||||
else
|
||||
echo "Sandbox mode rolled back to off due to partial sandbox config failure."
|
||||
fi
|
||||
if [[ -n "${SANDBOX_COMPOSE_FILE:-}" ]]; then
|
||||
rm -f "$SANDBOX_COMPOSE_FILE"
|
||||
fi
|
||||
# Ensure gateway service definition is reset without sandbox overlay mount.
|
||||
docker compose "${BASE_COMPOSE_ARGS[@]}" up -d --force-recreate openclaw-gateway
|
||||
fi
|
||||
else
|
||||
# Keep reruns deterministic: if sandbox is not active for this run, reset
|
||||
# persisted sandbox mode so future execs do not require docker.sock by stale
|
||||
# config alone.
|
||||
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
||||
config set agents.defaults.sandbox.mode "off" >/dev/null; then
|
||||
echo "WARNING: Failed to reset agents.defaults.sandbox.mode to off" >&2
|
||||
fi
|
||||
if [[ -f "$ROOT_DIR/docker-compose.sandbox.yml" ]]; then
|
||||
rm -f "$ROOT_DIR/docker-compose.sandbox.yml"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Gateway running with host port mapping."
|
||||
echo "Access from tailnet devices via the host's tailnet IP."
|
||||
echo "Config: $OPENCLAW_CONFIG_DIR"
|
||||
echo "Workspace: $OPENCLAW_WORKSPACE_DIR"
|
||||
echo "Token: $OPENCLAW_GATEWAY_TOKEN"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " ${COMPOSE_HINT} logs -f openclaw-gateway"
|
||||
echo " ${COMPOSE_HINT} exec openclaw-gateway node dist/index.js health --token \"$OPENCLAW_GATEWAY_TOKEN\""
|
||||
exec "$SCRIPT_PATH" "$@"
|
||||
|
||||
@ -1046,4 +1046,4 @@ node -e "import('./path/to/handler.ts').then(console.log)"
|
||||
- [CLI Reference: hooks](/cli/hooks)
|
||||
- [Bundled Hooks README](https://github.com/openclaw/openclaw/tree/main/src/hooks/bundled)
|
||||
- [Webhook Hooks](/automation/webhook)
|
||||
- [Configuration](/gateway/configuration#hooks)
|
||||
- [Configuration](/gateway/configuration-reference#hooks)
|
||||
|
||||
@ -116,7 +116,7 @@ Want “groups can only see folder X” instead of “no host access”? Keep `w
|
||||
|
||||
Related:
|
||||
|
||||
- Configuration keys and defaults: [Gateway configuration](/gateway/configuration#agentsdefaultssandbox)
|
||||
- Configuration keys and defaults: [Gateway configuration](/gateway/configuration-reference#agents-defaults-sandbox)
|
||||
- Debugging why a tool is blocked: [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated)
|
||||
- Bind mounts details: [Sandboxing](/gateway/sandboxing#custom-bind-mounts)
|
||||
|
||||
|
||||
@ -17,18 +17,18 @@ IRC ships as an extension plugin, but it is configured in the main config under
|
||||
1. Enable IRC config in `~/.openclaw/openclaw.json`.
|
||||
2. Set at least:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"channels": {
|
||||
"irc": {
|
||||
"enabled": true,
|
||||
"host": "irc.libera.chat",
|
||||
"port": 6697,
|
||||
"tls": true,
|
||||
"nick": "openclaw-bot",
|
||||
"channels": ["#openclaw"]
|
||||
}
|
||||
}
|
||||
channels: {
|
||||
irc: {
|
||||
enabled: true,
|
||||
host: "irc.libera.chat",
|
||||
port: 6697,
|
||||
tls: true,
|
||||
nick: "openclaw-bot",
|
||||
channels: ["#openclaw"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -75,7 +75,7 @@ If you see logs like:
|
||||
|
||||
Example (allow anyone in `#tuirc-dev` to talk to the bot):
|
||||
|
||||
```json5
|
||||
```json55
|
||||
{
|
||||
channels: {
|
||||
irc: {
|
||||
@ -96,7 +96,7 @@ That means you may see logs like `drop channel … (missing-mention)` unless the
|
||||
|
||||
To make the bot reply in an IRC channel **without needing a mention**, disable mention gating for that channel:
|
||||
|
||||
```json5
|
||||
```json55
|
||||
{
|
||||
channels: {
|
||||
irc: {
|
||||
@ -114,7 +114,7 @@ To make the bot reply in an IRC channel **without needing a mention**, disable m
|
||||
|
||||
Or to allow **all** IRC channels (no per-channel allowlist) and still reply without mentions:
|
||||
|
||||
```json5
|
||||
```json55
|
||||
{
|
||||
channels: {
|
||||
irc: {
|
||||
@ -134,7 +134,7 @@ To reduce risk, restrict tools for that channel.
|
||||
|
||||
### Same tools for everyone in the channel
|
||||
|
||||
```json5
|
||||
```json55
|
||||
{
|
||||
channels: {
|
||||
irc: {
|
||||
@ -155,7 +155,7 @@ To reduce risk, restrict tools for that channel.
|
||||
|
||||
Use `toolsBySender` to apply a stricter policy to `"*"` and a looser one to your nick:
|
||||
|
||||
```json5
|
||||
```json55
|
||||
{
|
||||
channels: {
|
||||
irc: {
|
||||
@ -190,32 +190,32 @@ For more on group access vs mention-gating (and how they interact), see: [/chann
|
||||
|
||||
To identify with NickServ after connect:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"channels": {
|
||||
"irc": {
|
||||
"nickserv": {
|
||||
"enabled": true,
|
||||
"service": "NickServ",
|
||||
"password": "your-nickserv-password"
|
||||
}
|
||||
}
|
||||
}
|
||||
channels: {
|
||||
irc: {
|
||||
nickserv: {
|
||||
enabled: true,
|
||||
service: "NickServ",
|
||||
password: "your-nickserv-password",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Optional one-time registration on connect:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"channels": {
|
||||
"irc": {
|
||||
"nickserv": {
|
||||
"register": true,
|
||||
"registerEmail": "bot@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
channels: {
|
||||
irc: {
|
||||
nickserv: {
|
||||
register: true,
|
||||
registerEmail: "bot@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -204,6 +204,8 @@ Bootstrap cross-signing and verification state:
|
||||
openclaw matrix verify bootstrap
|
||||
```
|
||||
|
||||
Multi-account support: use `channels.matrix.accounts` with per-account credentials and optional `name`. See [Configuration reference](/gateway/configuration-reference#multi-account-all-channels) for the shared pattern.
|
||||
|
||||
Verbose bootstrap diagnostics:
|
||||
|
||||
```bash
|
||||
|
||||
@ -260,15 +260,17 @@ This is often easier than hand-editing JSON manifests.
|
||||
|
||||
4. **Configure OpenClaw**
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"msteams": {
|
||||
"enabled": true,
|
||||
"appId": "<APP_ID>",
|
||||
"appPassword": "<APP_PASSWORD>",
|
||||
"tenantId": "<TENANT_ID>",
|
||||
"webhook": { "port": 3978, "path": "/api/messages" }
|
||||
}
|
||||
channels: {
|
||||
msteams: {
|
||||
enabled: true,
|
||||
appId: "<APP_ID>",
|
||||
appPassword: "<APP_PASSWORD>",
|
||||
tenantId: "<TENANT_ID>",
|
||||
webhook: { port: 3978, path: "/api/messages" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -312,49 +314,49 @@ These are the **existing resourceSpecific permissions** in our Teams app manifes
|
||||
|
||||
Minimal, valid example with the required fields. Replace IDs and URLs.
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
|
||||
"manifestVersion": "1.23",
|
||||
"version": "1.0.0",
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": { "short": "OpenClaw" },
|
||||
"developer": {
|
||||
"name": "Your Org",
|
||||
"websiteUrl": "https://example.com",
|
||||
"privacyUrl": "https://example.com/privacy",
|
||||
"termsOfUseUrl": "https://example.com/terms"
|
||||
$schema: "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
|
||||
manifestVersion: "1.23",
|
||||
version: "1.0.0",
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
name: { short: "OpenClaw" },
|
||||
developer: {
|
||||
name: "Your Org",
|
||||
websiteUrl: "https://example.com",
|
||||
privacyUrl: "https://example.com/privacy",
|
||||
termsOfUseUrl: "https://example.com/terms",
|
||||
},
|
||||
"description": { "short": "OpenClaw in Teams", "full": "OpenClaw in Teams" },
|
||||
"icons": { "outline": "outline.png", "color": "color.png" },
|
||||
"accentColor": "#5B6DEF",
|
||||
"bots": [
|
||||
description: { short: "OpenClaw in Teams", full: "OpenClaw in Teams" },
|
||||
icons: { outline: "outline.png", color: "color.png" },
|
||||
accentColor: "#5B6DEF",
|
||||
bots: [
|
||||
{
|
||||
"botId": "11111111-1111-1111-1111-111111111111",
|
||||
"scopes": ["personal", "team", "groupChat"],
|
||||
"isNotificationOnly": false,
|
||||
"supportsCalling": false,
|
||||
"supportsVideo": false,
|
||||
"supportsFiles": true
|
||||
}
|
||||
botId: "11111111-1111-1111-1111-111111111111",
|
||||
scopes: ["personal", "team", "groupChat"],
|
||||
isNotificationOnly: false,
|
||||
supportsCalling: false,
|
||||
supportsVideo: false,
|
||||
supportsFiles: true,
|
||||
},
|
||||
],
|
||||
"webApplicationInfo": {
|
||||
"id": "11111111-1111-1111-1111-111111111111"
|
||||
webApplicationInfo: {
|
||||
id: "11111111-1111-1111-1111-111111111111",
|
||||
},
|
||||
authorization: {
|
||||
permissions: {
|
||||
resourceSpecific: [
|
||||
{ name: "ChannelMessage.Read.Group", type: "Application" },
|
||||
{ name: "ChannelMessage.Send.Group", type: "Application" },
|
||||
{ name: "Member.Read.Group", type: "Application" },
|
||||
{ name: "Owner.Read.Group", type: "Application" },
|
||||
{ name: "ChannelSettings.Read.Group", type: "Application" },
|
||||
{ name: "TeamMember.Read.Group", type: "Application" },
|
||||
{ name: "TeamSettings.Read.Group", type: "Application" },
|
||||
{ name: "ChatMessage.Read.Chat", type: "Application" },
|
||||
],
|
||||
},
|
||||
},
|
||||
"authorization": {
|
||||
"permissions": {
|
||||
"resourceSpecific": [
|
||||
{ "name": "ChannelMessage.Read.Group", "type": "Application" },
|
||||
{ "name": "ChannelMessage.Send.Group", "type": "Application" },
|
||||
{ "name": "Member.Read.Group", "type": "Application" },
|
||||
{ "name": "Owner.Read.Group", "type": "Application" },
|
||||
{ "name": "ChannelSettings.Read.Group", "type": "Application" },
|
||||
{ "name": "TeamMember.Read.Group", "type": "Application" },
|
||||
{ "name": "TeamSettings.Read.Group", "type": "Application" },
|
||||
{ "name": "ChatMessage.Read.Chat", "type": "Application" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -500,20 +502,22 @@ Teams recently introduced two channel UI styles over the same underlying data mo
|
||||
|
||||
**Solution:** Configure `replyStyle` per-channel based on how the channel is set up:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"msteams": {
|
||||
"replyStyle": "thread",
|
||||
"teams": {
|
||||
"19:abc...@thread.tacv2": {
|
||||
"channels": {
|
||||
"19:xyz...@thread.tacv2": {
|
||||
"replyStyle": "top-level"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
channels: {
|
||||
msteams: {
|
||||
replyStyle: "thread",
|
||||
teams: {
|
||||
"19:abc...@thread.tacv2": {
|
||||
channels: {
|
||||
"19:xyz...@thread.tacv2": {
|
||||
replyStyle: "top-level",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -616,16 +620,16 @@ The `card` parameter accepts an Adaptive Card JSON object. When `card` is provid
|
||||
|
||||
**Agent tool:**
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"action": "send",
|
||||
"channel": "msteams",
|
||||
"target": "user:<id>",
|
||||
"card": {
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.5",
|
||||
"body": [{ "type": "TextBlock", "text": "Hello!" }]
|
||||
}
|
||||
action: "send",
|
||||
channel: "msteams",
|
||||
target: "user:<id>",
|
||||
card: {
|
||||
type: "AdaptiveCard",
|
||||
version: "1.5",
|
||||
body: [{ type: "TextBlock", text: "Hello!" }],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -669,25 +673,25 @@ openclaw message send --channel msteams --target "conversation:19:abc...@thread.
|
||||
|
||||
**Agent tool examples:**
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"action": "send",
|
||||
"channel": "msteams",
|
||||
"target": "user:John Smith",
|
||||
"message": "Hello!"
|
||||
action: "send",
|
||||
channel: "msteams",
|
||||
target: "user:John Smith",
|
||||
message: "Hello!",
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"action": "send",
|
||||
"channel": "msteams",
|
||||
"target": "conversation:19:abc...@thread.tacv2",
|
||||
"card": {
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.5",
|
||||
"body": [{ "type": "TextBlock", "text": "Hello" }]
|
||||
}
|
||||
action: "send",
|
||||
channel: "msteams",
|
||||
target: "conversation:19:abc...@thread.tacv2",
|
||||
card: {
|
||||
type: "AdaptiveCard",
|
||||
version: "1.5",
|
||||
body: [{ type: "TextBlock", text: "Hello" }],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -60,13 +60,13 @@ nak key generate
|
||||
|
||||
2. Add to config:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"channels": {
|
||||
"nostr": {
|
||||
"privateKey": "${NOSTR_PRIVATE_KEY}"
|
||||
}
|
||||
}
|
||||
channels: {
|
||||
nostr: {
|
||||
privateKey: "${NOSTR_PRIVATE_KEY}",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -96,23 +96,23 @@ Profile data is published as a NIP-01 `kind:0` event. You can manage it from the
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"channels": {
|
||||
"nostr": {
|
||||
"privateKey": "${NOSTR_PRIVATE_KEY}",
|
||||
"profile": {
|
||||
"name": "openclaw",
|
||||
"displayName": "OpenClaw",
|
||||
"about": "Personal assistant DM bot",
|
||||
"picture": "https://example.com/avatar.png",
|
||||
"banner": "https://example.com/banner.png",
|
||||
"website": "https://example.com",
|
||||
"nip05": "openclaw@example.com",
|
||||
"lud16": "openclaw@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
channels: {
|
||||
nostr: {
|
||||
privateKey: "${NOSTR_PRIVATE_KEY}",
|
||||
profile: {
|
||||
name: "openclaw",
|
||||
displayName: "OpenClaw",
|
||||
about: "Personal assistant DM bot",
|
||||
picture: "https://example.com/avatar.png",
|
||||
banner: "https://example.com/banner.png",
|
||||
website: "https://example.com",
|
||||
nip05: "openclaw@example.com",
|
||||
lud16: "openclaw@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -132,15 +132,15 @@ Notes:
|
||||
|
||||
### Allowlist example
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"channels": {
|
||||
"nostr": {
|
||||
"privateKey": "${NOSTR_PRIVATE_KEY}",
|
||||
"dmPolicy": "allowlist",
|
||||
"allowFrom": ["npub1abc...", "npub1xyz..."]
|
||||
}
|
||||
}
|
||||
channels: {
|
||||
nostr: {
|
||||
privateKey: "${NOSTR_PRIVATE_KEY}",
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["npub1abc...", "npub1xyz..."],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -155,14 +155,14 @@ Accepted formats:
|
||||
|
||||
Defaults: `relay.damus.io` and `nos.lol`.
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"channels": {
|
||||
"nostr": {
|
||||
"privateKey": "${NOSTR_PRIVATE_KEY}",
|
||||
"relays": ["wss://relay.damus.io", "wss://relay.primal.net", "wss://nostr.wine"]
|
||||
}
|
||||
}
|
||||
channels: {
|
||||
nostr: {
|
||||
privateKey: "${NOSTR_PRIVATE_KEY}",
|
||||
relays: ["wss://relay.damus.io", "wss://relay.primal.net", "wss://nostr.wine"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -191,14 +191,14 @@ Tips:
|
||||
docker run -p 7777:7777 ghcr.io/hoytech/strfry
|
||||
```
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"channels": {
|
||||
"nostr": {
|
||||
"privateKey": "${NOSTR_PRIVATE_KEY}",
|
||||
"relays": ["ws://localhost:7777"]
|
||||
}
|
||||
}
|
||||
channels: {
|
||||
nostr: {
|
||||
privateKey: "${NOSTR_PRIVATE_KEY}",
|
||||
relays: ["ws://localhost:7777"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -99,7 +99,7 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
|
||||
Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration-reference#multi-account-all-channels) for the shared pattern.
|
||||
|
||||
## Setup path B: register dedicated bot number (SMS, Linux)
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ Healthy baseline:
|
||||
| Group messages ignored | Check `requireMention` + mention patterns in config | Mention the bot or relax mention policy for that group. |
|
||||
| Random disconnect/relogin loops | `openclaw channels status --probe` + logs | Re-login and verify credentials directory is healthy. |
|
||||
|
||||
Full troubleshooting: [/channels/whatsapp#troubleshooting-quick](/channels/whatsapp#troubleshooting-quick)
|
||||
Full troubleshooting: [/channels/whatsapp#troubleshooting](/channels/whatsapp#troubleshooting)
|
||||
|
||||
## Telegram
|
||||
|
||||
@ -90,7 +90,7 @@ Full troubleshooting: [/channels/slack#troubleshooting](/channels/slack#troubles
|
||||
|
||||
Full troubleshooting:
|
||||
|
||||
- [/channels/imessage#troubleshooting-macos-privacy-and-security-tcc](/channels/imessage#troubleshooting-macos-privacy-and-security-tcc)
|
||||
- [/channels/imessage#troubleshooting](/channels/imessage#troubleshooting)
|
||||
- [/channels/bluebubbles#troubleshooting](/channels/bluebubbles#troubleshooting)
|
||||
|
||||
## Signal
|
||||
|
||||
@ -123,7 +123,7 @@ Prefer `allowFrom` for a hard allowlist. Use `allowedRoles` instead if you want
|
||||
|
||||
**Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent.
|
||||
|
||||
Find your Twitch user ID: [https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/) (Convert your Twitch username to ID)
|
||||
Find your Twitch user ID: [https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/) (Convert your Twitch username to ID)
|
||||
|
||||
## Token refresh (optional)
|
||||
|
||||
|
||||
@ -138,14 +138,24 @@ state dir extensions root (`$OPENCLAW_STATE_DIR/extensions/<id>`). Use
|
||||
### Update
|
||||
|
||||
```bash
|
||||
openclaw plugins update <id>
|
||||
openclaw plugins update <id-or-npm-spec>
|
||||
openclaw plugins update --all
|
||||
openclaw plugins update <id> --dry-run
|
||||
openclaw plugins update <id-or-npm-spec> --dry-run
|
||||
openclaw plugins update @openclaw/voice-call@beta
|
||||
```
|
||||
|
||||
Updates apply to tracked installs in `plugins.installs`, currently npm and
|
||||
marketplace installs.
|
||||
|
||||
When you pass a plugin id, OpenClaw reuses the recorded install spec for that
|
||||
plugin. That means previously stored dist-tags such as `@beta` and exact pinned
|
||||
versions continue to be used on later `update <id>` runs.
|
||||
|
||||
For npm installs, you can also pass an explicit npm package spec with a dist-tag
|
||||
or exact version. OpenClaw resolves that package name back to the tracked plugin
|
||||
record, updates that installed plugin, and records the new npm spec for future
|
||||
id-based updates.
|
||||
|
||||
When a stored integrity hash exists and the fetched artifact hash changes,
|
||||
OpenClaw prints a warning and asks for confirmation before proceeding. Use
|
||||
global `--yes` to bypass prompts in CI/non-interactive runs.
|
||||
|
||||
@ -46,7 +46,7 @@ JSON examples:
|
||||
"activeMinutes": null,
|
||||
"sessions": [
|
||||
{ "agentId": "main", "key": "agent:main:main", "model": "gpt-5" },
|
||||
{ "agentId": "work", "key": "agent:work:main", "model": "claude-opus-4-5" }
|
||||
{ "agentId": "work", "key": "agent:work:main", "model": "claude-opus-4-6" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
---
|
||||
summary: "Agent runtime (embedded pi-mono), workspace contract, and session bootstrap"
|
||||
summary: "Agent runtime, workspace contract, and session bootstrap"
|
||||
read_when:
|
||||
- Changing agent runtime, workspace bootstrap, or session behavior
|
||||
title: "Agent Runtime"
|
||||
---
|
||||
|
||||
# Agent Runtime 🤖
|
||||
# Agent Runtime
|
||||
|
||||
OpenClaw runs a single embedded agent runtime derived from **pi-mono**.
|
||||
OpenClaw runs a single embedded agent runtime.
|
||||
|
||||
## Workspace (required)
|
||||
|
||||
@ -63,12 +63,11 @@ OpenClaw loads skills from three locations (workspace wins on name conflict):
|
||||
|
||||
Skills can be gated by config/env (see `skills` in [Gateway configuration](/gateway/configuration)).
|
||||
|
||||
## pi-mono integration
|
||||
## Runtime boundaries
|
||||
|
||||
OpenClaw reuses pieces of the pi-mono codebase (models/tools), but **session management, discovery, and tool wiring are OpenClaw-owned**.
|
||||
|
||||
- No pi-coding agent runtime.
|
||||
- No `~/.pi/agent` or `<workspace>/.pi` settings are consulted.
|
||||
The embedded agent runtime is built on the Pi agent core (models, tools, and
|
||||
prompt pipeline). Session management, discovery, tool wiring, and channel
|
||||
delivery are OpenClaw-owned layers on top of that core.
|
||||
|
||||
## Sessions
|
||||
|
||||
@ -77,7 +76,7 @@ Session transcripts are stored as JSONL at:
|
||||
- `~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl`
|
||||
|
||||
The session ID is stable and chosen by OpenClaw.
|
||||
Legacy Pi/Tau session folders are **not** read.
|
||||
Legacy session folders from other tools are not read.
|
||||
|
||||
## Steering while streaming
|
||||
|
||||
|
||||
@ -7,8 +7,6 @@ title: "Gateway Architecture"
|
||||
|
||||
# Gateway architecture
|
||||
|
||||
Last updated: 2026-01-22
|
||||
|
||||
## Overview
|
||||
|
||||
- A single long‑lived **Gateway** owns all messaging surfaces (WhatsApp via
|
||||
|
||||
@ -31,7 +31,7 @@ You can optionally specify a different model for compaction summarization via `a
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"compaction": {
|
||||
"model": "openrouter/anthropic/claude-sonnet-4-5"
|
||||
"model": "openrouter/anthropic/claude-sonnet-4-6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,24 +32,42 @@ title: "Features"
|
||||
|
||||
## Full list
|
||||
|
||||
- WhatsApp integration via WhatsApp Web (Baileys)
|
||||
- Telegram bot support (grammY)
|
||||
- Discord bot support (channels.discord.js)
|
||||
- Mattermost bot support (plugin)
|
||||
- iMessage integration via local imsg CLI (macOS)
|
||||
- Agent bridge for Pi in RPC mode with tool streaming
|
||||
- Streaming and chunking for long responses
|
||||
- Multi-agent routing for isolated sessions per workspace or sender
|
||||
- Subscription auth for Anthropic and OpenAI via OAuth
|
||||
- Sessions: direct chats collapse into shared `main`; groups are isolated
|
||||
- Group chat support with mention based activation
|
||||
- Media support for images, audio, and documents
|
||||
- Optional voice note transcription hook
|
||||
- WebChat and macOS menu bar app
|
||||
- iOS node with pairing, Canvas, camera, screen recording, location, and voice features
|
||||
- Android node with pairing, Connect tab, chat sessions, voice tab, Canvas/camera, plus device, notifications, contacts/calendar, motion, photos, and SMS commands
|
||||
**Channels:**
|
||||
|
||||
<Note>
|
||||
Legacy Claude, Codex, Gemini, and Opencode paths have been removed. Pi is the only
|
||||
coding agent path.
|
||||
</Note>
|
||||
- WhatsApp, Telegram, Discord, iMessage (built-in)
|
||||
- Mattermost, Matrix, MS Teams, Nostr, and more (plugins)
|
||||
- Group chat support with mention-based activation
|
||||
- DM safety with allowlists and pairing
|
||||
|
||||
**Agent:**
|
||||
|
||||
- Embedded agent runtime with tool streaming
|
||||
- Multi-agent routing with isolated sessions per workspace or sender
|
||||
- Sessions: direct chats collapse into shared `main`; groups are isolated
|
||||
- Streaming and chunking for long responses
|
||||
|
||||
**Auth and providers:**
|
||||
|
||||
- 35+ model providers (Anthropic, OpenAI, Google, and more)
|
||||
- Subscription auth via OAuth (e.g. OpenAI Codex)
|
||||
- Custom and self-hosted provider support (vLLM, SGLang, Ollama, and any OpenAI-compatible or Anthropic-compatible endpoint)
|
||||
|
||||
**Media:**
|
||||
|
||||
- Images, audio, video, and documents in and out
|
||||
- Voice note transcription
|
||||
- Text-to-speech with multiple providers
|
||||
|
||||
**Apps and interfaces:**
|
||||
|
||||
- WebChat and browser Control UI
|
||||
- macOS menu bar companion app
|
||||
- iOS node with pairing, Canvas, camera, screen recording, location, and voice
|
||||
- Android node with pairing, chat, voice, Canvas, camera, and device commands
|
||||
|
||||
**Tools and automation:**
|
||||
|
||||
- Browser automation, exec, sandboxing
|
||||
- Web search (Brave, Perplexity, Gemini, Grok, Kimi, Firecrawl)
|
||||
- Cron jobs and heartbeat scheduling
|
||||
- Skills, plugins, and workflow pipelines (Lobster)
|
||||
|
||||
@ -34,8 +34,8 @@ These files live under the workspace (`agents.defaults.workspace`, default
|
||||
|
||||
OpenClaw exposes two agent-facing tools for these Markdown files:
|
||||
|
||||
- `memory_search` — semantic recall over indexed snippets.
|
||||
- `memory_get` — targeted read of a specific Markdown file/line range.
|
||||
- `memory_search` -- semantic recall over indexed snippets.
|
||||
- `memory_get` -- targeted read of a specific Markdown file/line range.
|
||||
|
||||
`memory_get` now **degrades gracefully when a file doesn't exist** (for example,
|
||||
today's daily log before the first write). Both the builtin manager and the QMD
|
||||
@ -94,709 +94,15 @@ For the full compaction lifecycle, see
|
||||
## Vector memory search
|
||||
|
||||
OpenClaw can build a small vector index over `MEMORY.md` and `memory/*.md` so
|
||||
semantic queries can find related notes even when wording differs.
|
||||
|
||||
Defaults:
|
||||
|
||||
- Enabled by default.
|
||||
- Watches memory files for changes (debounced).
|
||||
- Configure memory search under `agents.defaults.memorySearch` (not top-level
|
||||
`memorySearch`).
|
||||
- Uses remote embeddings by default. If `memorySearch.provider` is not set, OpenClaw auto-selects:
|
||||
1. `local` if a `memorySearch.local.modelPath` is configured and the file exists.
|
||||
2. `openai` if an OpenAI key can be resolved.
|
||||
3. `gemini` if a Gemini key can be resolved.
|
||||
4. `voyage` if a Voyage key can be resolved.
|
||||
5. `mistral` if a Mistral key can be resolved.
|
||||
6. Otherwise memory search stays disabled until configured.
|
||||
- Local mode uses node-llama-cpp and may require `pnpm approve-builds`.
|
||||
- Uses sqlite-vec (when available) to accelerate vector search inside SQLite.
|
||||
- `memorySearch.provider = "ollama"` is also supported for local/self-hosted
|
||||
Ollama embeddings (`/api/embeddings`), but it is not auto-selected.
|
||||
|
||||
Remote embeddings **require** an API key for the embedding provider. OpenClaw
|
||||
resolves keys from auth profiles, `models.providers.*.apiKey`, or environment
|
||||
variables. Codex OAuth only covers chat/completions and does **not** satisfy
|
||||
embeddings for memory search. For Gemini, use `GEMINI_API_KEY` or
|
||||
`models.providers.google.apiKey`. For Voyage, use `VOYAGE_API_KEY` or
|
||||
`models.providers.voyage.apiKey`. For Mistral, use `MISTRAL_API_KEY` or
|
||||
`models.providers.mistral.apiKey`. Ollama typically does not require a real API
|
||||
key (a placeholder like `OLLAMA_API_KEY=ollama-local` is enough when needed by
|
||||
local policy).
|
||||
When using a custom OpenAI-compatible endpoint,
|
||||
set `memorySearch.remote.apiKey` (and optional `memorySearch.remote.headers`).
|
||||
|
||||
### QMD backend (experimental)
|
||||
|
||||
Set `memory.backend = "qmd"` to swap the built-in SQLite indexer for
|
||||
[QMD](https://github.com/tobi/qmd): a local-first search sidecar that combines
|
||||
BM25 + vectors + reranking. Markdown stays the source of truth; OpenClaw shells
|
||||
out to QMD for retrieval. Key points:
|
||||
|
||||
**Prereqs**
|
||||
|
||||
- Disabled by default. Opt in per-config (`memory.backend = "qmd"`).
|
||||
- Install the QMD CLI separately (`bun install -g https://github.com/tobi/qmd` or grab
|
||||
a release) and make sure the `qmd` binary is on the gateway’s `PATH`.
|
||||
- QMD needs an SQLite build that allows extensions (`brew install sqlite` on
|
||||
macOS).
|
||||
- QMD runs fully locally via Bun + `node-llama-cpp` and auto-downloads GGUF
|
||||
models from HuggingFace on first use (no separate Ollama daemon required).
|
||||
- The gateway runs QMD in a self-contained XDG home under
|
||||
`~/.openclaw/agents/<agentId>/qmd/` by setting `XDG_CONFIG_HOME` and
|
||||
`XDG_CACHE_HOME`.
|
||||
- OS support: macOS and Linux work out of the box once Bun + SQLite are
|
||||
installed. Windows is best supported via WSL2.
|
||||
|
||||
**How the sidecar runs**
|
||||
|
||||
- The gateway writes a self-contained QMD home under
|
||||
`~/.openclaw/agents/<agentId>/qmd/` (config + cache + sqlite DB).
|
||||
- Collections are created via `qmd collection add` from `memory.qmd.paths`
|
||||
(plus default workspace memory files), then `qmd update` + `qmd embed` run
|
||||
on boot and on a configurable interval (`memory.qmd.update.interval`,
|
||||
default 5 m).
|
||||
- The gateway now initializes the QMD manager on startup, so periodic update
|
||||
timers are armed even before the first `memory_search` call.
|
||||
- Boot refresh now runs in the background by default so chat startup is not
|
||||
blocked; set `memory.qmd.update.waitForBootSync = true` to keep the previous
|
||||
blocking behavior.
|
||||
- Searches run via `memory.qmd.searchMode` (default `qmd search --json`; also
|
||||
supports `vsearch` and `query`). If the selected mode rejects flags on your
|
||||
QMD build, OpenClaw retries with `qmd query`. If QMD fails or the binary is
|
||||
missing, OpenClaw automatically falls back to the builtin SQLite manager so
|
||||
memory tools keep working.
|
||||
- OpenClaw does not expose QMD embed batch-size tuning today; batch behavior is
|
||||
controlled by QMD itself.
|
||||
- **First search may be slow**: QMD may download local GGUF models (reranker/query
|
||||
expansion) on the first `qmd query` run.
|
||||
- OpenClaw sets `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` automatically when it runs QMD.
|
||||
- If you want to pre-download models manually (and warm the same index OpenClaw
|
||||
uses), run a one-off query with the agent’s XDG dirs.
|
||||
|
||||
OpenClaw’s QMD state lives under your **state dir** (defaults to `~/.openclaw`).
|
||||
You can point `qmd` at the exact same index by exporting the same XDG vars
|
||||
OpenClaw uses:
|
||||
|
||||
```bash
|
||||
# Pick the same state dir OpenClaw uses
|
||||
STATE_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
|
||||
|
||||
export XDG_CONFIG_HOME="$STATE_DIR/agents/main/qmd/xdg-config"
|
||||
export XDG_CACHE_HOME="$STATE_DIR/agents/main/qmd/xdg-cache"
|
||||
|
||||
# (Optional) force an index refresh + embeddings
|
||||
qmd update
|
||||
qmd embed
|
||||
|
||||
# Warm up / trigger first-time model downloads
|
||||
qmd query "test" -c memory-root --json >/dev/null 2>&1
|
||||
```
|
||||
|
||||
**Config surface (`memory.qmd.*`)**
|
||||
|
||||
- `command` (default `qmd`): override the executable path.
|
||||
- `searchMode` (default `search`): pick which QMD command backs
|
||||
`memory_search` (`search`, `vsearch`, `query`).
|
||||
- `includeDefaultMemory` (default `true`): auto-index `MEMORY.md` + `memory/**/*.md`.
|
||||
- `paths[]`: add extra directories/files (`path`, optional `pattern`, optional
|
||||
stable `name`).
|
||||
- `sessions`: opt into session JSONL indexing (`enabled`, `retentionDays`,
|
||||
`exportDir`).
|
||||
- `update`: controls refresh cadence and maintenance execution:
|
||||
(`interval`, `debounceMs`, `onBoot`, `waitForBootSync`, `embedInterval`,
|
||||
`commandTimeoutMs`, `updateTimeoutMs`, `embedTimeoutMs`).
|
||||
- `limits`: clamp recall payload (`maxResults`, `maxSnippetChars`,
|
||||
`maxInjectedChars`, `timeoutMs`).
|
||||
- `scope`: same schema as [`session.sendPolicy`](/gateway/configuration#session).
|
||||
Default is DM-only (`deny` all, `allow` direct chats); loosen it to surface QMD
|
||||
hits in groups/channels.
|
||||
- `match.keyPrefix` matches the **normalized** session key (lowercased, with any
|
||||
leading `agent:<id>:` stripped). Example: `discord:channel:`.
|
||||
- `match.rawKeyPrefix` matches the **raw** session key (lowercased), including
|
||||
`agent:<id>:`. Example: `agent:main:discord:`.
|
||||
- Legacy: `match.keyPrefix: "agent:..."` is still treated as a raw-key prefix,
|
||||
but prefer `rawKeyPrefix` for clarity.
|
||||
- When `scope` denies a search, OpenClaw logs a warning with the derived
|
||||
`channel`/`chatType` so empty results are easier to debug.
|
||||
- Snippets sourced outside the workspace show up as
|
||||
`qmd/<collection>/<relative-path>` in `memory_search` results; `memory_get`
|
||||
understands that prefix and reads from the configured QMD collection root.
|
||||
- When `memory.qmd.sessions.enabled = true`, OpenClaw exports sanitized session
|
||||
transcripts (User/Assistant turns) into a dedicated QMD collection under
|
||||
`~/.openclaw/agents/<id>/qmd/sessions/`, so `memory_search` can recall recent
|
||||
conversations without touching the builtin SQLite index.
|
||||
- `memory_search` snippets now include a `Source: <path#line>` footer when
|
||||
`memory.citations` is `auto`/`on`; set `memory.citations = "off"` to keep
|
||||
the path metadata internal (the agent still receives the path for
|
||||
`memory_get`, but the snippet text omits the footer and the system prompt
|
||||
warns the agent not to cite it).
|
||||
|
||||
**Example**
|
||||
|
||||
```json5
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
citations: "auto",
|
||||
qmd: {
|
||||
includeDefaultMemory: true,
|
||||
update: { interval: "5m", debounceMs: 15000 },
|
||||
limits: { maxResults: 6, timeoutMs: 4000 },
|
||||
scope: {
|
||||
default: "deny",
|
||||
rules: [
|
||||
{ action: "allow", match: { chatType: "direct" } },
|
||||
// Normalized session-key prefix (strips `agent:<id>:`).
|
||||
{ action: "deny", match: { keyPrefix: "discord:channel:" } },
|
||||
// Raw session-key prefix (includes `agent:<id>:`).
|
||||
{ action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } },
|
||||
]
|
||||
},
|
||||
paths: [
|
||||
{ name: "docs", path: "~/notes", pattern: "**/*.md" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Citations & fallback**
|
||||
|
||||
- `memory.citations` applies regardless of backend (`auto`/`on`/`off`).
|
||||
- When `qmd` runs, we tag `status().backend = "qmd"` so diagnostics show which
|
||||
engine served the results. If the QMD subprocess exits or JSON output can’t be
|
||||
parsed, the search manager logs a warning and returns the builtin provider
|
||||
(existing Markdown embeddings) until QMD recovers.
|
||||
|
||||
### Additional memory paths
|
||||
|
||||
If you want to index Markdown files outside the default workspace layout, add
|
||||
explicit paths:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Paths can be absolute or workspace-relative.
|
||||
- Directories are scanned recursively for `.md` files.
|
||||
- By default, only Markdown files are indexed.
|
||||
- If `memorySearch.multimodal.enabled = true`, OpenClaw also indexes supported image/audio files under `extraPaths` only. Default memory roots (`MEMORY.md`, `memory.md`, `memory/**/*.md`) stay Markdown-only.
|
||||
- Symlinks are ignored (files or directories).
|
||||
|
||||
### Multimodal memory files (Gemini image + audio)
|
||||
|
||||
OpenClaw can index image and audio files from `memorySearch.extraPaths` when using Gemini embedding 2:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "gemini",
|
||||
model: "gemini-embedding-2-preview",
|
||||
extraPaths: ["assets/reference", "voice-notes"],
|
||||
multimodal: {
|
||||
enabled: true,
|
||||
modalities: ["image", "audio"], // or ["all"]
|
||||
maxFileBytes: 10000000
|
||||
},
|
||||
remote: {
|
||||
apiKey: "YOUR_GEMINI_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Multimodal memory is currently supported only for `gemini-embedding-2-preview`.
|
||||
- Multimodal indexing applies only to files discovered through `memorySearch.extraPaths`.
|
||||
- Supported modalities in this phase: image and audio.
|
||||
- `memorySearch.fallback` must stay `"none"` while multimodal memory is enabled.
|
||||
- Matching image/audio file bytes are uploaded to the configured Gemini embedding endpoint during indexing.
|
||||
- Supported image extensions: `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.heic`, `.heif`.
|
||||
- Supported audio extensions: `.mp3`, `.wav`, `.ogg`, `.opus`, `.m4a`, `.aac`, `.flac`.
|
||||
- Search queries remain text, but Gemini can compare those text queries against indexed image/audio embeddings.
|
||||
- `memory_get` still reads Markdown only; binary files are searchable but not returned as raw file contents.
|
||||
|
||||
### Gemini embeddings (native)
|
||||
|
||||
Set the provider to `gemini` to use the Gemini embeddings API directly:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "gemini",
|
||||
model: "gemini-embedding-001",
|
||||
remote: {
|
||||
apiKey: "YOUR_GEMINI_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `remote.baseUrl` is optional (defaults to the Gemini API base URL).
|
||||
- `remote.headers` lets you add extra headers if needed.
|
||||
- Default model: `gemini-embedding-001`.
|
||||
- `gemini-embedding-2-preview` is also supported: 8192 token limit and configurable dimensions (768 / 1536 / 3072, default 3072).
|
||||
|
||||
#### Gemini Embedding 2 (preview)
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "gemini",
|
||||
model: "gemini-embedding-2-preview",
|
||||
outputDimensionality: 3072, // optional: 768, 1536, or 3072 (default)
|
||||
remote: {
|
||||
apiKey: "YOUR_GEMINI_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **⚠️ Re-index required:** Switching from `gemini-embedding-001` (768 dimensions)
|
||||
> to `gemini-embedding-2-preview` (3072 dimensions) changes the vector size. The same is true if you
|
||||
> change `outputDimensionality` between 768, 1536, and 3072.
|
||||
> OpenClaw will automatically reindex when it detects a model or dimension change.
|
||||
|
||||
If you want to use a **custom OpenAI-compatible endpoint** (OpenRouter, vLLM, or a proxy),
|
||||
you can use the `remote` configuration with the OpenAI provider:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "openai",
|
||||
model: "text-embedding-3-small",
|
||||
remote: {
|
||||
baseUrl: "https://api.example.com/v1/",
|
||||
apiKey: "YOUR_OPENAI_COMPAT_API_KEY",
|
||||
headers: { "X-Custom-Header": "value" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you don't want to set an API key, use `memorySearch.provider = "local"` or set
|
||||
`memorySearch.fallback = "none"`.
|
||||
|
||||
Fallbacks:
|
||||
|
||||
- `memorySearch.fallback` can be `openai`, `gemini`, `voyage`, `mistral`, `ollama`, `local`, or `none`.
|
||||
- The fallback provider is only used when the primary embedding provider fails.
|
||||
|
||||
Batch indexing (OpenAI + Gemini + Voyage):
|
||||
|
||||
- Disabled by default. Set `agents.defaults.memorySearch.remote.batch.enabled = true` to enable for large-corpus indexing (OpenAI, Gemini, and Voyage).
|
||||
- Default behavior waits for batch completion; tune `remote.batch.wait`, `remote.batch.pollIntervalMs`, and `remote.batch.timeoutMinutes` if needed.
|
||||
- Set `remote.batch.concurrency` to control how many batch jobs we submit in parallel (default: 2).
|
||||
- Batch mode applies when `memorySearch.provider = "openai"` or `"gemini"` and uses the corresponding API key.
|
||||
- Gemini batch jobs use the async embeddings batch endpoint and require Gemini Batch API availability.
|
||||
|
||||
Why OpenAI batch is fast + cheap:
|
||||
|
||||
- For large backfills, OpenAI is typically the fastest option we support because we can submit many embedding requests in a single batch job and let OpenAI process them asynchronously.
|
||||
- OpenAI offers discounted pricing for Batch API workloads, so large indexing runs are usually cheaper than sending the same requests synchronously.
|
||||
- See the OpenAI Batch API docs and pricing for details:
|
||||
- [https://platform.openai.com/docs/api-reference/batch](https://platform.openai.com/docs/api-reference/batch)
|
||||
- [https://platform.openai.com/pricing](https://platform.openai.com/pricing)
|
||||
|
||||
Config example:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "openai",
|
||||
model: "text-embedding-3-small",
|
||||
fallback: "openai",
|
||||
remote: {
|
||||
batch: { enabled: true, concurrency: 2 }
|
||||
},
|
||||
sync: { watch: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Tools:
|
||||
|
||||
- `memory_search` — returns snippets with file + line ranges.
|
||||
- `memory_get` — read memory file content by path.
|
||||
|
||||
Local mode:
|
||||
|
||||
- Set `agents.defaults.memorySearch.provider = "local"`.
|
||||
- Provide `agents.defaults.memorySearch.local.modelPath` (GGUF or `hf:` URI).
|
||||
- Optional: set `agents.defaults.memorySearch.fallback = "none"` to avoid remote fallback.
|
||||
|
||||
### How the memory tools work
|
||||
|
||||
- `memory_search` semantically searches Markdown chunks (~400 token target, 80-token overlap) from `MEMORY.md` + `memory/**/*.md`. It returns snippet text (capped ~700 chars), file path, line range, score, provider/model, and whether we fell back from local → remote embeddings. No full file payload is returned.
|
||||
- `memory_get` reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are rejected.
|
||||
- Both tools are enabled only when `memorySearch.enabled` resolves true for the agent.
|
||||
|
||||
### What gets indexed (and when)
|
||||
|
||||
- File type: Markdown only (`MEMORY.md`, `memory/**/*.md`).
|
||||
- Index storage: per-agent SQLite at `~/.openclaw/memory/<agentId>.sqlite` (configurable via `agents.defaults.memorySearch.store.path`, supports `{agentId}` token).
|
||||
- Freshness: watcher on `MEMORY.md` + `memory/` marks the index dirty (debounce 1.5s). Sync is scheduled on session start, on search, or on an interval and runs asynchronously. Session transcripts use delta thresholds to trigger background sync.
|
||||
- Reindex triggers: the index stores the embedding **provider/model + endpoint fingerprint + chunking params**. If any of those change, OpenClaw automatically resets and reindexes the entire store.
|
||||
|
||||
### Hybrid search (BM25 + vector)
|
||||
|
||||
When enabled, OpenClaw combines:
|
||||
|
||||
- **Vector similarity** (semantic match, wording can differ)
|
||||
- **BM25 keyword relevance** (exact tokens like IDs, env vars, code symbols)
|
||||
|
||||
If full-text search is unavailable on your platform, OpenClaw falls back to vector-only search.
|
||||
|
||||
#### Why hybrid?
|
||||
|
||||
Vector search is great at “this means the same thing”:
|
||||
|
||||
- “Mac Studio gateway host” vs “the machine running the gateway”
|
||||
- “debounce file updates” vs “avoid indexing on every write”
|
||||
|
||||
But it can be weak at exact, high-signal tokens:
|
||||
|
||||
- IDs (`a828e60`, `b3b9895a…`)
|
||||
- code symbols (`memorySearch.query.hybrid`)
|
||||
- error strings ("sqlite-vec unavailable")
|
||||
|
||||
BM25 (full-text) is the opposite: strong at exact tokens, weaker at paraphrases.
|
||||
Hybrid search is the pragmatic middle ground: **use both retrieval signals** so you get
|
||||
good results for both "natural language" queries and "needle in a haystack" queries.
|
||||
|
||||
#### How we merge results (the current design)
|
||||
|
||||
Implementation sketch:
|
||||
|
||||
1. Retrieve a candidate pool from both sides:
|
||||
|
||||
- **Vector**: top `maxResults * candidateMultiplier` by cosine similarity.
|
||||
- **BM25**: top `maxResults * candidateMultiplier` by FTS5 BM25 rank (lower is better).
|
||||
|
||||
2. Convert BM25 rank into a 0..1-ish score:
|
||||
|
||||
- `textScore = 1 / (1 + max(0, bm25Rank))`
|
||||
|
||||
3. Union candidates by chunk id and compute a weighted score:
|
||||
|
||||
- `finalScore = vectorWeight * vectorScore + textWeight * textScore`
|
||||
|
||||
Notes:
|
||||
|
||||
- `vectorWeight` + `textWeight` is normalized to 1.0 in config resolution, so weights behave as percentages.
|
||||
- If embeddings are unavailable (or the provider returns a zero-vector), we still run BM25 and return keyword matches.
|
||||
- If FTS5 can't be created, we keep vector-only search (no hard failure).
|
||||
|
||||
This isn't "IR-theory perfect", but it's simple, fast, and tends to improve recall/precision on real notes.
|
||||
If we want to get fancier later, common next steps are Reciprocal Rank Fusion (RRF) or score normalization
|
||||
(min/max or z-score) before mixing.
|
||||
|
||||
#### Post-processing pipeline
|
||||
|
||||
After merging vector and keyword scores, two optional post-processing stages
|
||||
refine the result list before it reaches the agent:
|
||||
|
||||
```
|
||||
Vector + Keyword → Weighted Merge → Temporal Decay → Sort → MMR → Top-K Results
|
||||
```
|
||||
|
||||
Both stages are **off by default** and can be enabled independently.
|
||||
|
||||
#### MMR re-ranking (diversity)
|
||||
|
||||
When hybrid search returns results, multiple chunks may contain similar or overlapping content.
|
||||
For example, searching for "home network setup" might return five nearly identical snippets
|
||||
from different daily notes that all mention the same router configuration.
|
||||
|
||||
**MMR (Maximal Marginal Relevance)** re-ranks the results to balance relevance with diversity,
|
||||
ensuring the top results cover different aspects of the query instead of repeating the same information.
|
||||
|
||||
How it works:
|
||||
|
||||
1. Results are scored by their original relevance (vector + BM25 weighted score).
|
||||
2. MMR iteratively selects results that maximize: `λ × relevance − (1−λ) × max_similarity_to_selected`.
|
||||
3. Similarity between results is measured using Jaccard text similarity on tokenized content.
|
||||
|
||||
The `lambda` parameter controls the trade-off:
|
||||
|
||||
- `lambda = 1.0` → pure relevance (no diversity penalty)
|
||||
- `lambda = 0.0` → maximum diversity (ignores relevance)
|
||||
- Default: `0.7` (balanced, slight relevance bias)
|
||||
|
||||
**Example — query: "home network setup"**
|
||||
|
||||
Given these memory files:
|
||||
|
||||
```
|
||||
memory/2026-02-10.md → "Configured Omada router, set VLAN 10 for IoT devices"
|
||||
memory/2026-02-08.md → "Configured Omada router, moved IoT to VLAN 10"
|
||||
memory/2026-02-05.md → "Set up AdGuard DNS on 192.168.10.2"
|
||||
memory/network.md → "Router: Omada ER605, AdGuard: 192.168.10.2, VLAN 10: IoT"
|
||||
```
|
||||
|
||||
Without MMR — top 3 results:
|
||||
|
||||
```
|
||||
1. memory/2026-02-10.md (score: 0.92) ← router + VLAN
|
||||
2. memory/2026-02-08.md (score: 0.89) ← router + VLAN (near-duplicate!)
|
||||
3. memory/network.md (score: 0.85) ← reference doc
|
||||
```
|
||||
|
||||
With MMR (λ=0.7) — top 3 results:
|
||||
|
||||
```
|
||||
1. memory/2026-02-10.md (score: 0.92) ← router + VLAN
|
||||
2. memory/network.md (score: 0.85) ← reference doc (diverse!)
|
||||
3. memory/2026-02-05.md (score: 0.78) ← AdGuard DNS (diverse!)
|
||||
```
|
||||
|
||||
The near-duplicate from Feb 8 drops out, and the agent gets three distinct pieces of information.
|
||||
|
||||
**When to enable:** If you notice `memory_search` returning redundant or near-duplicate snippets,
|
||||
especially with daily notes that often repeat similar information across days.
|
||||
|
||||
#### Temporal decay (recency boost)
|
||||
|
||||
Agents with daily notes accumulate hundreds of dated files over time. Without decay,
|
||||
a well-worded note from six months ago can outrank yesterday's update on the same topic.
|
||||
|
||||
**Temporal decay** applies an exponential multiplier to scores based on the age of each result,
|
||||
so recent memories naturally rank higher while old ones fade:
|
||||
|
||||
```
|
||||
decayedScore = score × e^(-λ × ageInDays)
|
||||
```
|
||||
|
||||
where `λ = ln(2) / halfLifeDays`.
|
||||
|
||||
With the default half-life of 30 days:
|
||||
|
||||
- Today's notes: **100%** of original score
|
||||
- 7 days ago: **~84%**
|
||||
- 30 days ago: **50%**
|
||||
- 90 days ago: **12.5%**
|
||||
- 180 days ago: **~1.6%**
|
||||
|
||||
**Evergreen files are never decayed:**
|
||||
|
||||
- `MEMORY.md` (root memory file)
|
||||
- Non-dated files in `memory/` (e.g., `memory/projects.md`, `memory/network.md`)
|
||||
- These contain durable reference information that should always rank normally.
|
||||
|
||||
**Dated daily files** (`memory/YYYY-MM-DD.md`) use the date extracted from the filename.
|
||||
Other sources (e.g., session transcripts) fall back to file modification time (`mtime`).
|
||||
|
||||
**Example — query: "what's Rod's work schedule?"**
|
||||
|
||||
Given these memory files (today is Feb 10):
|
||||
|
||||
```
|
||||
memory/2025-09-15.md → "Rod works Mon-Fri, standup at 10am, pairing at 2pm" (148 days old)
|
||||
memory/2026-02-10.md → "Rod has standup at 14:15, 1:1 with Zeb at 14:45" (today)
|
||||
memory/2026-02-03.md → "Rod started new team, standup moved to 14:15" (7 days old)
|
||||
```
|
||||
|
||||
Without decay:
|
||||
|
||||
```
|
||||
1. memory/2025-09-15.md (score: 0.91) ← best semantic match, but stale!
|
||||
2. memory/2026-02-10.md (score: 0.82)
|
||||
3. memory/2026-02-03.md (score: 0.80)
|
||||
```
|
||||
|
||||
With decay (halfLife=30):
|
||||
|
||||
```
|
||||
1. memory/2026-02-10.md (score: 0.82 × 1.00 = 0.82) ← today, no decay
|
||||
2. memory/2026-02-03.md (score: 0.80 × 0.85 = 0.68) ← 7 days, mild decay
|
||||
3. memory/2025-09-15.md (score: 0.91 × 0.03 = 0.03) ← 148 days, nearly gone
|
||||
```
|
||||
|
||||
The stale September note drops to the bottom despite having the best raw semantic match.
|
||||
|
||||
**When to enable:** If your agent has months of daily notes and you find that old,
|
||||
stale information outranks recent context. A half-life of 30 days works well for
|
||||
daily-note-heavy workflows; increase it (e.g., 90 days) if you reference older notes frequently.
|
||||
|
||||
#### Configuration
|
||||
|
||||
Both features are configured under `memorySearch.query.hybrid`:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
query: {
|
||||
hybrid: {
|
||||
enabled: true,
|
||||
vectorWeight: 0.7,
|
||||
textWeight: 0.3,
|
||||
candidateMultiplier: 4,
|
||||
// Diversity: reduce redundant results
|
||||
mmr: {
|
||||
enabled: true, // default: false
|
||||
lambda: 0.7 // 0 = max diversity, 1 = max relevance
|
||||
},
|
||||
// Recency: boost newer memories
|
||||
temporalDecay: {
|
||||
enabled: true, // default: false
|
||||
halfLifeDays: 30 // score halves every 30 days
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can enable either feature independently:
|
||||
|
||||
- **MMR only** — useful when you have many similar notes but age doesn't matter.
|
||||
- **Temporal decay only** — useful when recency matters but your results are already diverse.
|
||||
- **Both** — recommended for agents with large, long-running daily note histories.
|
||||
|
||||
### Embedding cache
|
||||
|
||||
OpenClaw can cache **chunk embeddings** in SQLite so reindexing and frequent updates (especially session transcripts) don't re-embed unchanged text.
|
||||
|
||||
Config:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
cache: {
|
||||
enabled: true,
|
||||
maxEntries: 50000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Session memory search (experimental)
|
||||
|
||||
You can optionally index **session transcripts** and surface them via `memory_search`.
|
||||
This is gated behind an experimental flag.
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
experimental: { sessionMemory: true },
|
||||
sources: ["memory", "sessions"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Session indexing is **opt-in** (off by default).
|
||||
- Session updates are debounced and **indexed asynchronously** once they cross delta thresholds (best-effort).
|
||||
- `memory_search` never blocks on indexing; results can be slightly stale until background sync finishes.
|
||||
- Results still include snippets only; `memory_get` remains limited to memory files.
|
||||
- Session indexing is isolated per agent (only that agent’s session logs are indexed).
|
||||
- Session logs live on disk (`~/.openclaw/agents/<agentId>/sessions/*.jsonl`). Any process/user with filesystem access can read them, so treat disk access as the trust boundary. For stricter isolation, run agents under separate OS users or hosts.
|
||||
|
||||
Delta thresholds (defaults shown):
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
sync: {
|
||||
sessions: {
|
||||
deltaBytes: 100000, // ~100 KB
|
||||
deltaMessages: 50 // JSONL lines
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SQLite vector acceleration (sqlite-vec)
|
||||
|
||||
When the sqlite-vec extension is available, OpenClaw stores embeddings in a
|
||||
SQLite virtual table (`vec0`) and performs vector distance queries in the
|
||||
database. This keeps search fast without loading every embedding into JS.
|
||||
|
||||
Configuration (optional):
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
store: {
|
||||
vector: {
|
||||
enabled: true,
|
||||
extensionPath: "/path/to/sqlite-vec"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `enabled` defaults to true; when disabled, search falls back to in-process
|
||||
cosine similarity over stored embeddings.
|
||||
- If the sqlite-vec extension is missing or fails to load, OpenClaw logs the
|
||||
error and continues with the JS fallback (no vector table).
|
||||
- `extensionPath` overrides the bundled sqlite-vec path (useful for custom builds
|
||||
or non-standard install locations).
|
||||
|
||||
### Local embedding auto-download
|
||||
|
||||
- Default local embedding model: `hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf` (~0.6 GB).
|
||||
- When `memorySearch.provider = "local"`, `node-llama-cpp` resolves `modelPath`; if the GGUF is missing it **auto-downloads** to the cache (or `local.modelCacheDir` if set), then loads it. Downloads resume on retry.
|
||||
- Native build requirement: run `pnpm approve-builds`, pick `node-llama-cpp`, then `pnpm rebuild node-llama-cpp`.
|
||||
- Fallback: if local setup fails and `memorySearch.fallback = "openai"`, we automatically switch to remote embeddings (`openai/text-embedding-3-small` unless overridden) and record the reason.
|
||||
|
||||
### Custom OpenAI-compatible endpoint example
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "openai",
|
||||
model: "text-embedding-3-small",
|
||||
remote: {
|
||||
baseUrl: "https://api.example.com/v1/",
|
||||
apiKey: "YOUR_REMOTE_API_KEY",
|
||||
headers: {
|
||||
"X-Organization": "org-id",
|
||||
"X-Project": "project-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `remote.*` takes precedence over `models.providers.openai.*`.
|
||||
- `remote.headers` merge with OpenAI headers; remote wins on key conflicts. Omit `remote.headers` to use the OpenAI defaults.
|
||||
semantic queries can find related notes even when wording differs. Hybrid search
|
||||
(BM25 + vector) is available for combining semantic matching with exact keyword
|
||||
lookups.
|
||||
|
||||
Memory search supports multiple embedding providers (OpenAI, Gemini, Voyage,
|
||||
Mistral, Ollama, and local GGUF models), an optional QMD sidecar backend for
|
||||
advanced retrieval, and post-processing features like MMR diversity re-ranking
|
||||
and temporal decay.
|
||||
|
||||
For the full configuration reference -- including embedding provider setup, QMD
|
||||
backend, hybrid search tuning, multimodal memory, and all config knobs -- see
|
||||
[Memory configuration reference](/reference/memory-config).
|
||||
|
||||
@ -151,4 +151,4 @@ Outbound message formatting is centralized in `messages`:
|
||||
- `messages.responsePrefix`, `channels.<channel>.responsePrefix`, and `channels.<channel>.accounts.<id>.responsePrefix` (outbound prefix cascade), plus `channels.whatsapp.messagePrefix` (WhatsApp inbound prefix)
|
||||
- Reply threading via `replyToMode` and per-channel defaults
|
||||
|
||||
Details: [Configuration](/gateway/configuration#messages) and channel docs.
|
||||
Details: [Configuration](/gateway/configuration-reference#messages) and channel docs.
|
||||
|
||||
@ -277,7 +277,7 @@ See [/providers/kilocode](/providers/kilocode) for setup details.
|
||||
### Other bundled provider plugins
|
||||
|
||||
- OpenRouter: `openrouter` (`OPENROUTER_API_KEY`)
|
||||
- Example model: `openrouter/anthropic/claude-sonnet-4-5`
|
||||
- Example model: `openrouter/anthropic/claude-sonnet-4-6`
|
||||
- Kilo Gateway: `kilocode` (`KILOCODE_API_KEY`)
|
||||
- Example model: `kilocode/anthropic/claude-opus-4.6`
|
||||
- MiniMax: `minimax` (`MINIMAX_API_KEY`)
|
||||
|
||||
@ -58,7 +58,7 @@ Model refs are normalized to lowercase. Provider aliases like `z.ai/*` normalize
|
||||
to `zai/*`.
|
||||
|
||||
Provider configuration examples (including OpenCode) live in
|
||||
[/gateway/configuration](/gateway/configuration#opencode).
|
||||
[/providers/opencode](/providers/opencode).
|
||||
|
||||
## "Model is not allowed" (and why replies stop)
|
||||
|
||||
@ -82,9 +82,9 @@ Example allowlist config:
|
||||
```json5
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "anthropic/claude-sonnet-4-5" },
|
||||
model: { primary: "anthropic/claude-sonnet-4-6" },
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-5": { alias: "Sonnet" },
|
||||
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||
"anthropic/claude-opus-4-6": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
|
||||
@ -388,7 +388,7 @@ Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opu
|
||||
id: "chat",
|
||||
name: "Everyday",
|
||||
workspace: "~/.openclaw/workspace-chat",
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
{
|
||||
id: "opus",
|
||||
@ -422,7 +422,7 @@ Keep WhatsApp on the fast agent, but route one DM to Opus:
|
||||
id: "chat",
|
||||
name: "Everyday",
|
||||
workspace: "~/.openclaw/workspace-chat",
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
{
|
||||
id: "opus",
|
||||
@ -501,7 +501,7 @@ Notes:
|
||||
|
||||
## Per-Agent Sandbox and Tool Configuration
|
||||
|
||||
Starting with v2026.1.6, each agent can have its own sandbox and tool restrictions:
|
||||
Each agent can have its own sandbox and tool restrictions:
|
||||
|
||||
```js
|
||||
{
|
||||
|
||||
@ -50,7 +50,7 @@ Legacy import-only file (still supported, but not the main store):
|
||||
|
||||
- `~/.openclaw/credentials/oauth.json` (imported into `auth-profiles.json` on first use)
|
||||
|
||||
All of the above also respect `$OPENCLAW_STATE_DIR` (state dir override). Full reference: [/gateway/configuration](/gateway/configuration#auth-storage-oauth--api-keys)
|
||||
All of the above also respect `$OPENCLAW_STATE_DIR` (state dir override). Full reference: [/gateway/configuration](/gateway/configuration-reference#auth-storage)
|
||||
|
||||
For static secret refs and runtime snapshot activation behavior, see [Secrets Management](/gateway/secrets).
|
||||
|
||||
|
||||
254
docs/docs.json
254
docs/docs.json
@ -43,10 +43,39 @@
|
||||
"label": "Releases",
|
||||
"href": "https://github.com/openclaw/openclaw/releases",
|
||||
"icon": "package"
|
||||
},
|
||||
{
|
||||
"label": "Discord",
|
||||
"href": "https://discord.com/invite/clawd",
|
||||
"icon": "discord"
|
||||
}
|
||||
]
|
||||
},
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/platforms/oracle",
|
||||
"destination": "/install/oracle"
|
||||
},
|
||||
{
|
||||
"source": "/platforms/digitalocean",
|
||||
"destination": "/install/digitalocean"
|
||||
},
|
||||
{
|
||||
"source": "/platforms/raspberry-pi",
|
||||
"destination": "/install/raspberry-pi"
|
||||
},
|
||||
{
|
||||
"source": "/brave-search",
|
||||
"destination": "/tools/brave-search"
|
||||
},
|
||||
{
|
||||
"source": "/perplexity",
|
||||
"destination": "/tools/perplexity-search"
|
||||
},
|
||||
{
|
||||
"source": "/tts",
|
||||
"destination": "/tools/tts"
|
||||
},
|
||||
{
|
||||
"source": "/messages",
|
||||
"destination": "/concepts/messages"
|
||||
@ -824,17 +853,9 @@
|
||||
{
|
||||
"tab": "Get started",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Home",
|
||||
"pages": ["index"]
|
||||
},
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["start/showcase"]
|
||||
},
|
||||
{
|
||||
"group": "Core concepts",
|
||||
"pages": ["concepts/features"]
|
||||
"pages": ["index", "start/showcase", "concepts/features"]
|
||||
},
|
||||
{
|
||||
"group": "First steps",
|
||||
@ -860,41 +881,46 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Install overview",
|
||||
"pages": ["install/index", "install/installer"]
|
||||
"pages": ["install/index", "install/installer", "install/node"]
|
||||
},
|
||||
{
|
||||
"group": "Other install methods",
|
||||
"group": "Containers",
|
||||
"pages": [
|
||||
"install/docker",
|
||||
"install/podman",
|
||||
"install/nix",
|
||||
"install/ansible",
|
||||
"install/bun"
|
||||
"install/bun",
|
||||
"install/docker",
|
||||
"install/nix",
|
||||
"install/podman"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Hosting",
|
||||
"pages": [
|
||||
"install/azure",
|
||||
"install/digitalocean",
|
||||
"install/docker-vm-runtime",
|
||||
"install/exe-dev",
|
||||
"install/fly",
|
||||
"install/gcp",
|
||||
"install/hetzner",
|
||||
"install/kubernetes",
|
||||
"vps",
|
||||
"install/macos-vm",
|
||||
"install/northflank",
|
||||
"install/oracle",
|
||||
"install/railway",
|
||||
"install/raspberry-pi",
|
||||
"install/render"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Maintenance",
|
||||
"pages": ["install/updating", "install/migrating", "install/uninstall"]
|
||||
},
|
||||
{
|
||||
"group": "Hosting and deployment",
|
||||
"pages": [
|
||||
"vps",
|
||||
"install/kubernetes",
|
||||
"install/fly",
|
||||
"install/hetzner",
|
||||
"install/gcp",
|
||||
"install/azure",
|
||||
"install/macos-vm",
|
||||
"install/exe-dev",
|
||||
"install/railway",
|
||||
"install/render",
|
||||
"install/northflank"
|
||||
"install/updating",
|
||||
"install/migrating",
|
||||
"install/uninstall",
|
||||
"install/development-channels"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Advanced",
|
||||
"pages": ["install/development-channels"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -951,7 +977,6 @@
|
||||
{
|
||||
"group": "Fundamentals",
|
||||
"pages": [
|
||||
"pi",
|
||||
"concepts/architecture",
|
||||
"concepts/agent",
|
||||
"concepts/agent-loop",
|
||||
@ -959,13 +984,10 @@
|
||||
"concepts/context",
|
||||
"concepts/context-engine",
|
||||
"concepts/agent-workspace",
|
||||
"concepts/oauth"
|
||||
"concepts/oauth",
|
||||
"start/bootstrapping"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Bootstrapping",
|
||||
"pages": ["start/bootstrapping"]
|
||||
},
|
||||
{
|
||||
"group": "Sessions and memory",
|
||||
"pages": [
|
||||
@ -1002,7 +1024,7 @@
|
||||
"group": "Built-in tools",
|
||||
"pages": [
|
||||
"tools/apply-patch",
|
||||
"brave-search",
|
||||
"tools/brave-search",
|
||||
"tools/btw",
|
||||
"tools/diffs",
|
||||
"tools/elevated",
|
||||
@ -1013,7 +1035,7 @@
|
||||
"tools/lobster",
|
||||
"tools/loop-detection",
|
||||
"tools/pdf",
|
||||
"perplexity",
|
||||
"tools/perplexity-search",
|
||||
"tools/reactions",
|
||||
"tools/thinking",
|
||||
"tools/web"
|
||||
@ -1024,7 +1046,8 @@
|
||||
"pages": [
|
||||
"tools/browser",
|
||||
"tools/browser-login",
|
||||
"tools/browser-linux-troubleshooting"
|
||||
"tools/browser-linux-troubleshooting",
|
||||
"tools/browser-wsl2-windows-remote-cdp-troubleshooting"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1044,7 +1067,8 @@
|
||||
"tools/skills",
|
||||
"tools/skills-config",
|
||||
"tools/clawhub",
|
||||
"tools/plugin"
|
||||
"tools/plugin",
|
||||
"prose"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1058,8 +1082,7 @@
|
||||
"plugins/zalouser",
|
||||
"plugins/manifest",
|
||||
"plugins/agent-tools",
|
||||
"tools/capability-cookbook",
|
||||
"prose"
|
||||
"tools/capability-cookbook"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1087,7 +1110,7 @@
|
||||
"nodes/talk",
|
||||
"nodes/voicewake",
|
||||
"nodes/location-command",
|
||||
"tts"
|
||||
"tools/tts"
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -1100,12 +1123,8 @@
|
||||
"pages": ["providers/index", "providers/models"]
|
||||
},
|
||||
{
|
||||
"group": "Model concepts",
|
||||
"pages": ["concepts/models"]
|
||||
},
|
||||
{
|
||||
"group": "Configuration",
|
||||
"pages": ["concepts/model-providers", "concepts/model-failover"]
|
||||
"group": "Concepts and configuration",
|
||||
"pages": ["concepts/models", "concepts/model-providers", "concepts/model-failover"]
|
||||
},
|
||||
{
|
||||
"group": "Providers",
|
||||
@ -1117,6 +1136,7 @@
|
||||
"providers/deepgram",
|
||||
"providers/github-copilot",
|
||||
"providers/google",
|
||||
"providers/groq",
|
||||
"providers/huggingface",
|
||||
"providers/kilocode",
|
||||
"providers/litellm",
|
||||
@ -1159,10 +1179,7 @@
|
||||
"platforms/linux",
|
||||
"platforms/windows",
|
||||
"platforms/android",
|
||||
"platforms/ios",
|
||||
"platforms/digitalocean",
|
||||
"platforms/oracle",
|
||||
"platforms/raspberry-pi"
|
||||
"platforms/ios"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1211,6 +1228,7 @@
|
||||
"gateway/heartbeat",
|
||||
"gateway/doctor",
|
||||
"gateway/logging",
|
||||
"logging",
|
||||
"gateway/gateway-lock",
|
||||
"gateway/background-process",
|
||||
"gateway/multiple-gateways",
|
||||
@ -1241,6 +1259,7 @@
|
||||
{
|
||||
"group": "Networking and discovery",
|
||||
"pages": [
|
||||
"network",
|
||||
"gateway/network-model",
|
||||
"gateway/pairing",
|
||||
"gateway/discovery",
|
||||
@ -1274,51 +1293,76 @@
|
||||
"group": "CLI commands",
|
||||
"pages": [
|
||||
"cli/index",
|
||||
"cli/acp",
|
||||
"cli/agent",
|
||||
"cli/agents",
|
||||
"cli/approvals",
|
||||
"cli/browser",
|
||||
"cli/channels",
|
||||
"cli/clawbot",
|
||||
"cli/completion",
|
||||
"cli/config",
|
||||
"cli/configure",
|
||||
"cli/cron",
|
||||
"cli/daemon",
|
||||
"cli/dashboard",
|
||||
"cli/devices",
|
||||
"cli/directory",
|
||||
"cli/dns",
|
||||
"cli/docs",
|
||||
"cli/doctor",
|
||||
"cli/gateway",
|
||||
"cli/health",
|
||||
"cli/hooks",
|
||||
"cli/logs",
|
||||
"cli/memory",
|
||||
"cli/message",
|
||||
"cli/models",
|
||||
"cli/node",
|
||||
"cli/nodes",
|
||||
"cli/onboard",
|
||||
"cli/pairing",
|
||||
"cli/plugins",
|
||||
"cli/qr",
|
||||
"cli/reset",
|
||||
"cli/sandbox",
|
||||
"cli/secrets",
|
||||
"cli/security",
|
||||
"cli/sessions",
|
||||
"cli/setup",
|
||||
"cli/skills",
|
||||
"cli/status",
|
||||
"cli/system",
|
||||
"cli/tui",
|
||||
"cli/uninstall",
|
||||
"cli/update",
|
||||
"cli/voicecall",
|
||||
"cli/webhooks"
|
||||
{
|
||||
"group": "Gateway and service",
|
||||
"pages": [
|
||||
"cli/backup",
|
||||
"cli/daemon",
|
||||
"cli/doctor",
|
||||
"cli/gateway",
|
||||
"cli/health",
|
||||
"cli/logs",
|
||||
"cli/onboard",
|
||||
"cli/reset",
|
||||
"cli/secrets",
|
||||
"cli/security",
|
||||
"cli/setup",
|
||||
"cli/status",
|
||||
"cli/uninstall",
|
||||
"cli/update"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Agents and sessions",
|
||||
"pages": [
|
||||
"cli/agent",
|
||||
"cli/agents",
|
||||
"cli/hooks",
|
||||
"cli/memory",
|
||||
"cli/message",
|
||||
"cli/models",
|
||||
"cli/sessions",
|
||||
"cli/system"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Channels and messaging",
|
||||
"pages": [
|
||||
"cli/channels",
|
||||
"cli/devices",
|
||||
"cli/directory",
|
||||
"cli/pairing",
|
||||
"cli/qr",
|
||||
"cli/voicecall"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Tools and execution",
|
||||
"pages": [
|
||||
"cli/approvals",
|
||||
"cli/browser",
|
||||
"cli/cron",
|
||||
"cli/node",
|
||||
"cli/nodes",
|
||||
"cli/sandbox"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Configuration",
|
||||
"pages": ["cli/config", "cli/configure", "cli/webhooks"]
|
||||
},
|
||||
{
|
||||
"group": "Plugins and skills",
|
||||
"pages": ["cli/plugins", "cli/skills"]
|
||||
},
|
||||
{
|
||||
"group": "Interfaces",
|
||||
"pages": ["cli/dashboard", "cli/tui"]
|
||||
},
|
||||
{
|
||||
"group": "Utility",
|
||||
"pages": ["cli/acp", "cli/clawbot", "cli/completion", "cli/dns", "cli/docs"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1342,12 +1386,14 @@
|
||||
{
|
||||
"group": "Technical reference",
|
||||
"pages": [
|
||||
"pi",
|
||||
"reference/wizard",
|
||||
"reference/token-use",
|
||||
"reference/secretref-credential-surface",
|
||||
"reference/prompt-caching",
|
||||
"reference/api-usage-costs",
|
||||
"reference/transcript-hygiene",
|
||||
"reference/memory-config",
|
||||
"date-time"
|
||||
]
|
||||
},
|
||||
@ -1393,10 +1439,6 @@
|
||||
"diagnostics/flags"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Node runtime",
|
||||
"pages": ["install/node"]
|
||||
},
|
||||
{
|
||||
"group": "Compaction internals",
|
||||
"pages": ["reference/session-management-compaction"]
|
||||
|
||||
@ -114,8 +114,8 @@ The provider id becomes the left side of your model ref:
|
||||
modelArg: "--model",
|
||||
modelAliases: {
|
||||
"claude-opus-4-6": "opus",
|
||||
"claude-opus-4-5": "opus",
|
||||
"claude-sonnet-4-5": "sonnet",
|
||||
"claude-opus-4-6": "opus",
|
||||
"claude-sonnet-4-6": "sonnet",
|
||||
},
|
||||
sessionArg: "--session",
|
||||
sessionMode: "existing",
|
||||
|
||||
@ -35,7 +35,7 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
|
||||
},
|
||||
agent: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: { primary: "anthropic/claude-sonnet-4-5" },
|
||||
model: { primary: "anthropic/claude-sonnet-4-6" },
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
@ -238,15 +238,15 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
|
||||
workspace: "~/.openclaw/workspace",
|
||||
userTimezone: "America/Chicago",
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4-5",
|
||||
primary: "anthropic/claude-sonnet-4-6",
|
||||
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
|
||||
},
|
||||
imageModel: {
|
||||
primary: "openrouter/anthropic/claude-sonnet-4-5",
|
||||
primary: "openrouter/anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-opus-4-6": { alias: "opus" },
|
||||
"anthropic/claude-sonnet-4-5": { alias: "sonnet" },
|
||||
"anthropic/claude-sonnet-4-6": { alias: "sonnet" },
|
||||
"openai/gpt-5.2": { alias: "gpt" },
|
||||
},
|
||||
thinkingDefault: "low",
|
||||
@ -271,7 +271,7 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
|
||||
maxConcurrent: 3,
|
||||
heartbeat: {
|
||||
every: "30m",
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
target: "last",
|
||||
directPolicy: "allow", // allow (default) | block
|
||||
to: "+15555550123",
|
||||
@ -520,7 +520,7 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero
|
||||
agent: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4-5",
|
||||
primary: "anthropic/claude-sonnet-4-6",
|
||||
fallbacks: ["anthropic/claude-opus-4-6"],
|
||||
},
|
||||
},
|
||||
|
||||
@ -1019,7 +1019,7 @@ Periodic heartbeat runs.
|
||||
identifierPolicy: "strict", // strict | off | custom
|
||||
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
|
||||
postCompactionSections: ["Session Startup", "Red Lines"], // [] disables reinjection
|
||||
model: "openrouter/anthropic/claude-sonnet-4-5", // optional compaction-only model override
|
||||
model: "openrouter/anthropic/claude-sonnet-4-6", // optional compaction-only model override
|
||||
memoryFlush: {
|
||||
enabled: true,
|
||||
softThresholdTokens: 6000,
|
||||
|
||||
@ -112,11 +112,11 @@ When validation fails:
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4-5",
|
||||
primary: "anthropic/claude-sonnet-4-6",
|
||||
fallbacks: ["openai/gpt-5.2"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-5": { alias: "Sonnet" },
|
||||
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||
"openai/gpt-5.2": { alias: "GPT" },
|
||||
},
|
||||
},
|
||||
@ -251,7 +251,7 @@ When validation fails:
|
||||
|
||||
Build the image first: `scripts/sandbox-setup.sh`
|
||||
|
||||
See [Sandboxing](/gateway/sandboxing) for the full guide and [full reference](/gateway/configuration-reference#sandbox) for all options.
|
||||
See [Sandboxing](/gateway/sandboxing) for the full guide and [full reference](/gateway/configuration-reference#agents-defaults-sandbox) for all options.
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@ -69,11 +69,11 @@ Keep hosted models configured even when running local; use `models.mode: "merge"
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4-5",
|
||||
primary: "anthropic/claude-sonnet-4-6",
|
||||
fallbacks: ["lmstudio/minimax-m2.5-gs32", "anthropic/claude-opus-4-6"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-5": { alias: "Sonnet" },
|
||||
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||
"lmstudio/minimax-m2.5-gs32": { alias: "MiniMax Local" },
|
||||
"anthropic/claude-opus-4-6": { alias: "Opus" },
|
||||
},
|
||||
|
||||
@ -399,7 +399,7 @@ Security defaults:
|
||||
Docker installs and the containerized gateway live here:
|
||||
[Docker](/install/docker)
|
||||
|
||||
For Docker gateway deployments, `docker-setup.sh` can bootstrap sandbox config.
|
||||
For Docker gateway deployments, `scripts/docker/setup.sh` can bootstrap sandbox config.
|
||||
Set `OPENCLAW_SANDBOX=1` (or `true`/`yes`/`on`) to enable that path. You can
|
||||
override socket location with `OPENCLAW_DOCKER_SOCKET`. Full setup and env
|
||||
reference: [Docker](/install/docker#enable-agent-sandbox-for-docker-gateway-opt-in).
|
||||
@ -463,7 +463,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
|
||||
## Related docs
|
||||
|
||||
- [OpenShell](/gateway/openshell) -- managed sandbox backend setup, workspace modes, and config reference
|
||||
- [Sandbox Configuration](/gateway/configuration#agentsdefaults-sandbox)
|
||||
- [Sandbox Configuration](/gateway/configuration-reference#agents-defaults-sandbox)
|
||||
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) -- debugging "why is this blocked?"
|
||||
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) -- per-agent overrides and precedence
|
||||
- [Security](/gateway/security)
|
||||
|
||||
@ -5,7 +5,7 @@ read_when:
|
||||
title: "Security"
|
||||
---
|
||||
|
||||
# Security 🔒
|
||||
# Security
|
||||
|
||||
> [!WARNING]
|
||||
> **Personal assistant trust model:** this guidance assumes one trusted operator boundary per gateway (single-user/personal assistant model).
|
||||
@ -25,7 +25,7 @@ This page explains hardening **within that model**. It does not claim hostile mu
|
||||
|
||||
## Quick check: `openclaw security audit`
|
||||
|
||||
See also: [Formal Verification (Security Models)](/security/formal-verification/)
|
||||
See also: [Formal Verification (Security Models)](/security/formal-verification)
|
||||
|
||||
Run this regularly (especially after changing config or exposing network surfaces):
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ You can reference env vars directly in config string values using `${VAR_NAME}`
|
||||
}
|
||||
```
|
||||
|
||||
See [Configuration: Env var substitution](/gateway/configuration#env-var-substitution-in-config) for full details.
|
||||
See [Configuration: Env var substitution](/gateway/configuration-reference#env-var-substitution) for full details.
|
||||
|
||||
## Secret refs vs `${ENV}` strings
|
||||
|
||||
|
||||
4139
docs/help/faq.md
4139
docs/help/faq.md
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@ title: "Help"
|
||||
If you want a quick “get unstuck” flow, start here:
|
||||
|
||||
- **Troubleshooting:** [Start here](/help/troubleshooting)
|
||||
- **Install sanity (Node/npm/PATH):** [Install](/install#nodejs--npm-path-sanity)
|
||||
- **Install sanity (Node/npm/PATH):** [Install](/install/node#troubleshooting)
|
||||
- **Gateway issues:** [Gateway troubleshooting](/gateway/troubleshooting)
|
||||
- **Logs:** [Logging](/logging) and [Gateway logging](/gateway/logging)
|
||||
- **Repairs:** [Doctor](/gateway/doctor)
|
||||
@ -19,3 +19,10 @@ If you want a quick “get unstuck” flow, start here:
|
||||
If you’re looking for conceptual questions (not “something broke”):
|
||||
|
||||
- [FAQ (concepts)](/help/faq)
|
||||
|
||||
## Environment and debugging
|
||||
|
||||
- **Environment variables:** [Where OpenClaw loads env vars and precedence](/help/environment)
|
||||
- **Debugging:** [Watch mode, raw streams, and dev profile](/help/debugging)
|
||||
- **Testing:** [Test suites, live tests, and Docker runners](/help/testing)
|
||||
- **Scripts:** [Repository helper scripts](/help/scripts)
|
||||
|
||||
@ -308,7 +308,7 @@ This is the “common models” run we expect to keep working:
|
||||
|
||||
- OpenAI (non-Codex): `openai/gpt-5.2` (optional: `openai/gpt-5.1`)
|
||||
- OpenAI Codex: `openai-codex/gpt-5.4`
|
||||
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-5`)
|
||||
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-6`)
|
||||
- Google (Gemini API): `google/gemini-3.1-pro-preview` and `google/gemini-3-flash-preview` (avoid older Gemini 2.x models)
|
||||
- Google (Antigravity): `google-antigravity/claude-opus-4-6-thinking` and `google-antigravity/gemini-3-flash`
|
||||
- Z.AI (GLM): `zai/glm-4.7`
|
||||
@ -322,7 +322,7 @@ Run gateway smoke with tools + image:
|
||||
Pick at least one per provider family:
|
||||
|
||||
- OpenAI: `openai/gpt-5.2` (or `openai/gpt-5-mini`)
|
||||
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-5`)
|
||||
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-6`)
|
||||
- Google: `google/gemini-3-flash-preview` (or `google/gemini-3.1-pro-preview`)
|
||||
- Z.AI (GLM): `zai/glm-4.7`
|
||||
- MiniMax: `minimax/minimax-m2.5`
|
||||
|
||||
@ -106,15 +106,19 @@ The Gateway is the single source of truth for sessions, routing, and channel con
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
</Step>
|
||||
<Step title="Pair WhatsApp and start the Gateway">
|
||||
<Step title="Chat">
|
||||
Open the Control UI in your browser and send a message:
|
||||
|
||||
```bash
|
||||
openclaw channels login
|
||||
openclaw gateway --port 18789
|
||||
openclaw dashboard
|
||||
```
|
||||
|
||||
Or connect a channel ([Telegram](/channels/telegram) is fastest) and chat from your phone.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
Need the full install and dev setup? See [Quick start](/start/quickstart).
|
||||
Need the full install and dev setup? See [Getting Started](/start/getting-started).
|
||||
|
||||
## Dashboard
|
||||
|
||||
|
||||
@ -9,7 +9,29 @@ title: "Ansible"
|
||||
|
||||
# Ansible Installation
|
||||
|
||||
The recommended way to deploy OpenClaw to production servers is via **[openclaw-ansible](https://github.com/openclaw/openclaw-ansible)** — an automated installer with security-first architecture.
|
||||
Deploy OpenClaw to production servers with **[openclaw-ansible](https://github.com/openclaw/openclaw-ansible)** -- an automated installer with security-first architecture.
|
||||
|
||||
<Info>
|
||||
The [openclaw-ansible](https://github.com/openclaw/openclaw-ansible) repo is the source of truth for Ansible deployment. This page is a quick overview.
|
||||
</Info>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Requirement | Details |
|
||||
| ----------- | --------------------------------------------------------- |
|
||||
| **OS** | Debian 11+ or Ubuntu 20.04+ |
|
||||
| **Access** | Root or sudo privileges |
|
||||
| **Network** | Internet connection for package installation |
|
||||
| **Ansible** | 2.14+ (installed automatically by the quick-start script) |
|
||||
|
||||
## What You Get
|
||||
|
||||
- **Firewall-first security** -- UFW + Docker isolation (only SSH + Tailscale accessible)
|
||||
- **Tailscale VPN** -- secure remote access without exposing services publicly
|
||||
- **Docker** -- isolated sandbox containers, localhost-only bindings
|
||||
- **Defense in depth** -- 4-layer security architecture
|
||||
- **Systemd integration** -- auto-start on boot with hardening
|
||||
- **One-command setup** -- complete deployment in minutes
|
||||
|
||||
## Quick Start
|
||||
|
||||
@ -19,55 +41,50 @@ One-command install:
|
||||
curl -fsSL https://raw.githubusercontent.com/openclaw/openclaw-ansible/main/install.sh | bash
|
||||
```
|
||||
|
||||
> **📦 Full guide: [github.com/openclaw/openclaw-ansible](https://github.com/openclaw/openclaw-ansible)**
|
||||
>
|
||||
> The openclaw-ansible repo is the source of truth for Ansible deployment. This page is a quick overview.
|
||||
|
||||
## What You Get
|
||||
|
||||
- 🔒 **Firewall-first security**: UFW + Docker isolation (only SSH + Tailscale accessible)
|
||||
- 🔐 **Tailscale VPN**: Secure remote access without exposing services publicly
|
||||
- 🐳 **Docker**: Isolated sandbox containers, localhost-only bindings
|
||||
- 🛡️ **Defense in depth**: 4-layer security architecture
|
||||
- 🚀 **One-command setup**: Complete deployment in minutes
|
||||
- 🔧 **Systemd integration**: Auto-start on boot with hardening
|
||||
|
||||
## Requirements
|
||||
|
||||
- **OS**: Debian 11+ or Ubuntu 20.04+
|
||||
- **Access**: Root or sudo privileges
|
||||
- **Network**: Internet connection for package installation
|
||||
- **Ansible**: 2.14+ (installed automatically by quick-start script)
|
||||
|
||||
## What Gets Installed
|
||||
|
||||
The Ansible playbook installs and configures:
|
||||
|
||||
1. **Tailscale** (mesh VPN for secure remote access)
|
||||
2. **UFW firewall** (SSH + Tailscale ports only)
|
||||
3. **Docker CE + Compose V2** (for agent sandboxes)
|
||||
4. **Node.js 24 + pnpm** (runtime dependencies; Node 22 LTS, currently `22.16+`, remains supported for compatibility)
|
||||
5. **OpenClaw** (host-based, not containerized)
|
||||
6. **Systemd service** (auto-start with security hardening)
|
||||
1. **Tailscale** -- mesh VPN for secure remote access
|
||||
2. **UFW firewall** -- SSH + Tailscale ports only
|
||||
3. **Docker CE + Compose V2** -- for agent sandboxes
|
||||
4. **Node.js 24 + pnpm** -- runtime dependencies (Node 22 LTS, currently `22.16+`, remains supported)
|
||||
5. **OpenClaw** -- host-based, not containerized
|
||||
6. **Systemd service** -- auto-start with security hardening
|
||||
|
||||
Note: The gateway runs **directly on the host** (not in Docker), but agent sandboxes use Docker for isolation. See [Sandboxing](/gateway/sandboxing) for details.
|
||||
<Note>
|
||||
The gateway runs directly on the host (not in Docker), but agent sandboxes use Docker for isolation. See [Sandboxing](/gateway/sandboxing) for details.
|
||||
</Note>
|
||||
|
||||
## Post-Install Setup
|
||||
|
||||
After installation completes, switch to the openclaw user:
|
||||
<Steps>
|
||||
<Step title="Switch to the openclaw user">
|
||||
```bash
|
||||
sudo -i -u openclaw
|
||||
```
|
||||
</Step>
|
||||
<Step title="Run the onboarding wizard">
|
||||
The post-install script guides you through configuring OpenClaw settings.
|
||||
</Step>
|
||||
<Step title="Connect messaging providers">
|
||||
Log in to WhatsApp, Telegram, Discord, or Signal:
|
||||
```bash
|
||||
openclaw channels login
|
||||
```
|
||||
</Step>
|
||||
<Step title="Verify the installation">
|
||||
```bash
|
||||
sudo systemctl status openclaw
|
||||
sudo journalctl -u openclaw -f
|
||||
```
|
||||
</Step>
|
||||
<Step title="Connect to Tailscale">
|
||||
Join your VPN mesh for secure remote access.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
```bash
|
||||
sudo -i -u openclaw
|
||||
```
|
||||
|
||||
The post-install script will guide you through:
|
||||
|
||||
1. **Onboarding wizard**: Configure OpenClaw settings
|
||||
2. **Provider login**: Connect WhatsApp/Telegram/Discord/Signal
|
||||
3. **Gateway testing**: Verify the installation
|
||||
4. **Tailscale setup**: Connect to your VPN mesh
|
||||
|
||||
### Quick commands
|
||||
### Quick Commands
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
@ -86,115 +103,120 @@ openclaw channels login
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### 4-Layer Defense
|
||||
The deployment uses a 4-layer defense model:
|
||||
|
||||
1. **Firewall (UFW)**: Only SSH (22) + Tailscale (41641/udp) exposed publicly
|
||||
2. **VPN (Tailscale)**: Gateway accessible only via VPN mesh
|
||||
3. **Docker Isolation**: DOCKER-USER iptables chain prevents external port exposure
|
||||
4. **Systemd Hardening**: NoNewPrivileges, PrivateTmp, unprivileged user
|
||||
1. **Firewall (UFW)** -- only SSH (22) + Tailscale (41641/udp) exposed publicly
|
||||
2. **VPN (Tailscale)** -- gateway accessible only via VPN mesh
|
||||
3. **Docker isolation** -- DOCKER-USER iptables chain prevents external port exposure
|
||||
4. **Systemd hardening** -- NoNewPrivileges, PrivateTmp, unprivileged user
|
||||
|
||||
### Verification
|
||||
|
||||
Test external attack surface:
|
||||
To verify your external attack surface:
|
||||
|
||||
```bash
|
||||
nmap -p- YOUR_SERVER_IP
|
||||
```
|
||||
|
||||
Should show **only port 22** (SSH) open. All other services (gateway, Docker) are locked down.
|
||||
Only port 22 (SSH) should be open. All other services (gateway, Docker) are locked down.
|
||||
|
||||
### Docker Availability
|
||||
|
||||
Docker is installed for **agent sandboxes** (isolated tool execution), not for running the gateway itself. The gateway binds to localhost only and is accessible via Tailscale VPN.
|
||||
|
||||
See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for sandbox configuration.
|
||||
Docker is installed for agent sandboxes (isolated tool execution), not for running the gateway itself. See [Multi-Agent Sandbox and Tools](/tools/multi-agent-sandbox-tools) for sandbox configuration.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
If you prefer manual control over the automation:
|
||||
|
||||
```bash
|
||||
# 1. Install prerequisites
|
||||
sudo apt update && sudo apt install -y ansible git
|
||||
<Steps>
|
||||
<Step title="Install prerequisites">
|
||||
```bash
|
||||
sudo apt update && sudo apt install -y ansible git
|
||||
```
|
||||
</Step>
|
||||
<Step title="Clone the repository">
|
||||
```bash
|
||||
git clone https://github.com/openclaw/openclaw-ansible.git
|
||||
cd openclaw-ansible
|
||||
```
|
||||
</Step>
|
||||
<Step title="Install Ansible collections">
|
||||
```bash
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
```
|
||||
</Step>
|
||||
<Step title="Run the playbook">
|
||||
```bash
|
||||
./run-playbook.sh
|
||||
```
|
||||
|
||||
# 2. Clone repository
|
||||
git clone https://github.com/openclaw/openclaw-ansible.git
|
||||
cd openclaw-ansible
|
||||
Alternatively, run directly and then manually execute the setup script afterward:
|
||||
```bash
|
||||
ansible-playbook playbook.yml --ask-become-pass
|
||||
# Then run: /tmp/openclaw-setup.sh
|
||||
```
|
||||
|
||||
# 3. Install Ansible collections
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
# 4. Run playbook
|
||||
./run-playbook.sh
|
||||
|
||||
# Or run directly (then manually execute /tmp/openclaw-setup.sh after)
|
||||
# ansible-playbook playbook.yml --ask-become-pass
|
||||
```
|
||||
|
||||
## Updating OpenClaw
|
||||
## Updating
|
||||
|
||||
The Ansible installer sets up OpenClaw for manual updates. See [Updating](/install/updating) for the standard update flow.
|
||||
|
||||
To re-run the Ansible playbook (e.g., for configuration changes):
|
||||
To re-run the Ansible playbook (for example, for configuration changes):
|
||||
|
||||
```bash
|
||||
cd openclaw-ansible
|
||||
./run-playbook.sh
|
||||
```
|
||||
|
||||
Note: This is idempotent and safe to run multiple times.
|
||||
This is idempotent and safe to run multiple times.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Firewall blocks my connection
|
||||
<AccordionGroup>
|
||||
<Accordion title="Firewall blocks my connection">
|
||||
- Ensure you can access via Tailscale VPN first
|
||||
- SSH access (port 22) is always allowed
|
||||
- The gateway is only accessible via Tailscale by design
|
||||
</Accordion>
|
||||
<Accordion title="Service will not start">
|
||||
```bash
|
||||
# Check logs
|
||||
sudo journalctl -u openclaw -n 100
|
||||
|
||||
If you're locked out:
|
||||
# Verify permissions
|
||||
sudo ls -la /opt/openclaw
|
||||
|
||||
- Ensure you can access via Tailscale VPN first
|
||||
- SSH access (port 22) is always allowed
|
||||
- The gateway is **only** accessible via Tailscale by design
|
||||
# Test manual start
|
||||
sudo -i -u openclaw
|
||||
cd ~/openclaw
|
||||
openclaw gateway run
|
||||
```
|
||||
|
||||
### Service will not start
|
||||
</Accordion>
|
||||
<Accordion title="Docker sandbox issues">
|
||||
```bash
|
||||
# Verify Docker is running
|
||||
sudo systemctl status docker
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
sudo journalctl -u openclaw -n 100
|
||||
# Check sandbox image
|
||||
sudo docker images | grep openclaw-sandbox
|
||||
|
||||
# Verify permissions
|
||||
sudo ls -la /opt/openclaw
|
||||
# Build sandbox image if missing
|
||||
cd /opt/openclaw/openclaw
|
||||
sudo -u openclaw ./scripts/sandbox-setup.sh
|
||||
```
|
||||
|
||||
# Test manual start
|
||||
sudo -i -u openclaw
|
||||
cd ~/openclaw
|
||||
pnpm start
|
||||
```
|
||||
|
||||
### Docker sandbox issues
|
||||
|
||||
```bash
|
||||
# Verify Docker is running
|
||||
sudo systemctl status docker
|
||||
|
||||
# Check sandbox image
|
||||
sudo docker images | grep openclaw-sandbox
|
||||
|
||||
# Build sandbox image if missing
|
||||
cd /opt/openclaw/openclaw
|
||||
sudo -u openclaw ./scripts/sandbox-setup.sh
|
||||
```
|
||||
|
||||
### Provider login fails
|
||||
|
||||
Make sure you're running as the `openclaw` user:
|
||||
|
||||
```bash
|
||||
sudo -i -u openclaw
|
||||
openclaw channels login
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="Provider login fails">
|
||||
Make sure you are running as the `openclaw` user:
|
||||
```bash
|
||||
sudo -i -u openclaw
|
||||
openclaw channels login
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
For detailed security architecture and troubleshooting:
|
||||
For detailed security architecture and troubleshooting, see the openclaw-ansible repo:
|
||||
|
||||
- [Security Architecture](https://github.com/openclaw/openclaw-ansible/blob/main/docs/security.md)
|
||||
- [Technical Details](https://github.com/openclaw/openclaw-ansible/blob/main/docs/architecture.md)
|
||||
@ -202,7 +224,7 @@ For detailed security architecture and troubleshooting:
|
||||
|
||||
## Related
|
||||
|
||||
- [openclaw-ansible](https://github.com/openclaw/openclaw-ansible) — full deployment guide
|
||||
- [Docker](/install/docker) — containerized gateway setup
|
||||
- [Sandboxing](/gateway/sandboxing) — agent sandbox configuration
|
||||
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) — per-agent isolation
|
||||
- [openclaw-ansible](https://github.com/openclaw/openclaw-ansible) -- full deployment guide
|
||||
- [Docker](/install/docker) -- containerized gateway setup
|
||||
- [Sandboxing](/gateway/sandboxing) -- agent sandbox configuration
|
||||
- [Multi-Agent Sandbox and Tools](/tools/multi-agent-sandbox-tools) -- per-agent isolation
|
||||
|
||||
@ -27,139 +27,148 @@ You’ll need:
|
||||
- An Azure subscription with permission to create compute and network resources
|
||||
- Azure CLI installed (see [Azure CLI install steps](https://learn.microsoft.com/cli/azure/install-azure-cli) if needed)
|
||||
|
||||
## 1) Sign in to Azure CLI
|
||||
<Steps>
|
||||
<Step title="Sign in to Azure CLI">
|
||||
```bash
|
||||
az login # Sign in and select your Azure subscription
|
||||
az extension add -n ssh # Extension required for Azure Bastion SSH management
|
||||
```
|
||||
</Step>
|
||||
|
||||
```bash
|
||||
az login # Sign in and select your Azure subscription
|
||||
az extension add -n ssh # Extension required for Azure Bastion SSH management
|
||||
```
|
||||
<Step title="Register required resource providers (one-time)">
|
||||
```bash
|
||||
az provider register --namespace Microsoft.Compute
|
||||
az provider register --namespace Microsoft.Network
|
||||
```
|
||||
|
||||
## 2) Register required resource providers (one-time)
|
||||
Verify Azure resource provider registration. Wait until both show `Registered`.
|
||||
|
||||
```bash
|
||||
az provider register --namespace Microsoft.Compute
|
||||
az provider register --namespace Microsoft.Network
|
||||
```
|
||||
```bash
|
||||
az provider show --namespace Microsoft.Compute --query registrationState -o tsv
|
||||
az provider show --namespace Microsoft.Network --query registrationState -o tsv
|
||||
```
|
||||
|
||||
Verify Azure resource provider registration. Wait until both show `Registered`.
|
||||
</Step>
|
||||
|
||||
```bash
|
||||
az provider show --namespace Microsoft.Compute --query registrationState -o tsv
|
||||
az provider show --namespace Microsoft.Network --query registrationState -o tsv
|
||||
```
|
||||
<Step title="Set deployment variables">
|
||||
```bash
|
||||
RG="rg-openclaw"
|
||||
LOCATION="westus2"
|
||||
TEMPLATE_URI="https://raw.githubusercontent.com/openclaw/openclaw/main/infra/azure/templates/azuredeploy.json"
|
||||
PARAMS_URI="https://raw.githubusercontent.com/openclaw/openclaw/main/infra/azure/templates/azuredeploy.parameters.json"
|
||||
```
|
||||
</Step>
|
||||
|
||||
## 3) Set deployment variables
|
||||
<Step title="Select SSH key">
|
||||
Use your existing public key if you have one:
|
||||
|
||||
```bash
|
||||
RG="rg-openclaw"
|
||||
LOCATION="westus2"
|
||||
TEMPLATE_URI="https://raw.githubusercontent.com/openclaw/openclaw/main/infra/azure/templates/azuredeploy.json"
|
||||
PARAMS_URI="https://raw.githubusercontent.com/openclaw/openclaw/main/infra/azure/templates/azuredeploy.parameters.json"
|
||||
```
|
||||
```bash
|
||||
SSH_PUB_KEY="$(cat ~/.ssh/id_ed25519.pub)"
|
||||
```
|
||||
|
||||
## 4) Select SSH key
|
||||
If you don’t have an SSH key yet, run the following:
|
||||
|
||||
Use your existing public key if you have one:
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519 -C "you@example.com"
|
||||
SSH_PUB_KEY="$(cat ~/.ssh/id_ed25519.pub)"
|
||||
```
|
||||
|
||||
```bash
|
||||
SSH_PUB_KEY="$(cat ~/.ssh/id_ed25519.pub)"
|
||||
```
|
||||
</Step>
|
||||
|
||||
If you don’t have an SSH key yet, run the following:
|
||||
<Step title="Select VM size and OS disk size">
|
||||
Set VM and disk sizing variables:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519 -C "you@example.com"
|
||||
SSH_PUB_KEY="$(cat ~/.ssh/id_ed25519.pub)"
|
||||
```
|
||||
```bash
|
||||
VM_SIZE="Standard_B2as_v2"
|
||||
OS_DISK_SIZE_GB=64
|
||||
```
|
||||
|
||||
## 5) Select VM size and OS disk size
|
||||
Choose a VM size and OS disk size that are available in your Azure subscription/region and matches your workload:
|
||||
|
||||
Set VM and disk sizing variables:
|
||||
- Start smaller for light usage and scale up later
|
||||
- Use more vCPU/RAM/OS disk size for heavier automation, more channels, or larger model/tool workloads
|
||||
- If a VM size is unavailable in your region or subscription quota, pick the closest available SKU
|
||||
|
||||
```bash
|
||||
VM_SIZE="Standard_B2as_v2"
|
||||
OS_DISK_SIZE_GB=64
|
||||
```
|
||||
List VM sizes available in your target region:
|
||||
|
||||
Choose a VM size and OS disk size that are available in your Azure subscription/region and matches your workload:
|
||||
```bash
|
||||
az vm list-skus --location "${LOCATION}" --resource-type virtualMachines -o table
|
||||
```
|
||||
|
||||
- Start smaller for light usage and scale up later
|
||||
- Use more vCPU/RAM/OS disk size for heavier automation, more channels, or larger model/tool workloads
|
||||
- If a VM size is unavailable in your region or subscription quota, pick the closest available SKU
|
||||
Check your current VM vCPU and OS disk size usage/quota:
|
||||
|
||||
List VM sizes available in your target region:
|
||||
```bash
|
||||
az vm list-usage --location "${LOCATION}" -o table
|
||||
```
|
||||
|
||||
```bash
|
||||
az vm list-skus --location "${LOCATION}" --resource-type virtualMachines -o table
|
||||
```
|
||||
</Step>
|
||||
|
||||
Check your current VM vCPU and OS disk size usage/quota:
|
||||
<Step title="Create the resource group">
|
||||
```bash
|
||||
az group create -n "${RG}" -l "${LOCATION}"
|
||||
```
|
||||
</Step>
|
||||
|
||||
```bash
|
||||
az vm list-usage --location "${LOCATION}" -o table
|
||||
```
|
||||
<Step title="Deploy resources">
|
||||
This command applies your selected SSH key, VM size, and OS disk size.
|
||||
|
||||
## 6) Create the resource group
|
||||
```bash
|
||||
az deployment group create \
|
||||
-g "${RG}" \
|
||||
--template-uri "${TEMPLATE_URI}" \
|
||||
--parameters "${PARAMS_URI}" \
|
||||
--parameters location="${LOCATION}" \
|
||||
--parameters vmSize="${VM_SIZE}" \
|
||||
--parameters osDiskSizeGb="${OS_DISK_SIZE_GB}" \
|
||||
--parameters sshPublicKey="${SSH_PUB_KEY}"
|
||||
```
|
||||
|
||||
```bash
|
||||
az group create -n "${RG}" -l "${LOCATION}"
|
||||
```
|
||||
</Step>
|
||||
|
||||
## 7) Deploy resources
|
||||
<Step title="SSH into the VM through Azure Bastion">
|
||||
```bash
|
||||
RG="rg-openclaw"
|
||||
VM_NAME="vm-openclaw"
|
||||
BASTION_NAME="bas-openclaw"
|
||||
ADMIN_USERNAME="openclaw"
|
||||
VM_ID="$(az vm show -g "${RG}" -n "${VM_NAME}" --query id -o tsv)"
|
||||
|
||||
This command applies your selected SSH key, VM size, and OS disk size.
|
||||
az network bastion ssh \
|
||||
--name "${BASTION_NAME}" \
|
||||
--resource-group "${RG}" \
|
||||
--target-resource-id "${VM_ID}" \
|
||||
--auth-type ssh-key \
|
||||
--username "${ADMIN_USERNAME}" \
|
||||
--ssh-key ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
```bash
|
||||
az deployment group create \
|
||||
-g "${RG}" \
|
||||
--template-uri "${TEMPLATE_URI}" \
|
||||
--parameters "${PARAMS_URI}" \
|
||||
--parameters location="${LOCATION}" \
|
||||
--parameters vmSize="${VM_SIZE}" \
|
||||
--parameters osDiskSizeGb="${OS_DISK_SIZE_GB}" \
|
||||
--parameters sshPublicKey="${SSH_PUB_KEY}"
|
||||
```
|
||||
</Step>
|
||||
|
||||
## 8) SSH into the VM through Azure Bastion
|
||||
<Step title="Install OpenClaw (in the VM shell)">
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh -o /tmp/openclaw-install.sh
|
||||
bash /tmp/openclaw-install.sh
|
||||
rm -f /tmp/openclaw-install.sh
|
||||
openclaw --version
|
||||
```
|
||||
|
||||
```bash
|
||||
RG="rg-openclaw"
|
||||
VM_NAME="vm-openclaw"
|
||||
BASTION_NAME="bas-openclaw"
|
||||
ADMIN_USERNAME="openclaw"
|
||||
VM_ID="$(az vm show -g "${RG}" -n "${VM_NAME}" --query id -o tsv)"
|
||||
The installer script handles Node detection/installation and runs onboarding by default.
|
||||
|
||||
az network bastion ssh \
|
||||
--name "${BASTION_NAME}" \
|
||||
--resource-group "${RG}" \
|
||||
--target-resource-id "${VM_ID}" \
|
||||
--auth-type ssh-key \
|
||||
--username "${ADMIN_USERNAME}" \
|
||||
--ssh-key ~/.ssh/id_ed25519
|
||||
```
|
||||
</Step>
|
||||
|
||||
## 9) Install OpenClaw (in the VM shell)
|
||||
<Step title="Verify the Gateway">
|
||||
After onboarding completes:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh -o /tmp/openclaw-install.sh
|
||||
bash /tmp/openclaw-install.sh
|
||||
rm -f /tmp/openclaw-install.sh
|
||||
openclaw --version
|
||||
```
|
||||
```bash
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
The installer script handles Node detection/installation and runs onboarding by default.
|
||||
Most enterprise Azure teams already have GitHub Copilot licenses. If that is your case, we recommend choosing the GitHub Copilot provider in the OpenClaw onboarding wizard. See [GitHub Copilot provider](/providers/github-copilot).
|
||||
|
||||
## 10) Verify the Gateway
|
||||
The included ARM template uses Ubuntu image `version: "latest"` for convenience. If you need reproducible builds, pin a specific image version in `infra/azure/templates/azuredeploy.json` (you can list versions with `az vm image list --publisher Canonical --offer ubuntu-24_04-lts --sku server --all -o table`).
|
||||
|
||||
After onboarding completes:
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
Most enterprise Azure teams already have GitHub Copilot licenses. If that is your case, we recommend choosing the GitHub Copilot provider in the OpenClaw onboarding wizard. See [GitHub Copilot provider](/providers/github-copilot).
|
||||
|
||||
The included ARM template uses Ubuntu image `version: "latest"` for convenience. If you need reproducible builds, pin a specific image version in `infra/azure/templates/azuredeploy.json` (you can list versions with `az vm image list --publisher Canonical --offer ubuntu-24_04-lts --sku server --all -o table`).
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
@ -6,49 +6,45 @@ read_when:
|
||||
title: "Bun (Experimental)"
|
||||
---
|
||||
|
||||
# Bun (experimental)
|
||||
# Bun (Experimental)
|
||||
|
||||
Goal: run this repo with **Bun** (optional, not recommended for WhatsApp/Telegram)
|
||||
without diverging from pnpm workflows.
|
||||
<Warning>
|
||||
Bun is **not recommended for gateway runtime** (known issues with WhatsApp and Telegram). Use Node for production.
|
||||
</Warning>
|
||||
|
||||
⚠️ **Not recommended for Gateway runtime** (WhatsApp/Telegram bugs). Use Node for production.
|
||||
|
||||
## Status
|
||||
|
||||
- Bun is an optional local runtime for running TypeScript directly (`bun run …`, `bun --watch …`).
|
||||
- `pnpm` is the default for builds and remains fully supported (and used by some docs tooling).
|
||||
- Bun cannot use `pnpm-lock.yaml` and will ignore it.
|
||||
Bun is an optional local runtime for running TypeScript directly (`bun run ...`, `bun --watch ...`). The default package manager remains `pnpm`, which is fully supported and used by docs tooling. Bun cannot use `pnpm-lock.yaml` and will ignore it.
|
||||
|
||||
## Install
|
||||
|
||||
Default:
|
||||
<Steps>
|
||||
<Step title="Install dependencies">
|
||||
```sh
|
||||
bun install
|
||||
```
|
||||
|
||||
```sh
|
||||
bun install
|
||||
```
|
||||
`bun.lock` / `bun.lockb` are gitignored, so there is no repo churn. To skip lockfile writes entirely:
|
||||
|
||||
Note: `bun.lock`/`bun.lockb` are gitignored, so there’s no repo churn either way. If you want _no lockfile writes_:
|
||||
```sh
|
||||
bun install --no-save
|
||||
```
|
||||
|
||||
```sh
|
||||
bun install --no-save
|
||||
```
|
||||
</Step>
|
||||
<Step title="Build and test">
|
||||
```sh
|
||||
bun run build
|
||||
bun run vitest run
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Build / Test (Bun)
|
||||
## Lifecycle Scripts
|
||||
|
||||
```sh
|
||||
bun run build
|
||||
bun run vitest run
|
||||
```
|
||||
Bun blocks dependency lifecycle scripts unless explicitly trusted. For this repo, the commonly blocked scripts are not required:
|
||||
|
||||
## Bun lifecycle scripts (blocked by default)
|
||||
- `@whiskeysockets/baileys` `preinstall` -- checks Node major >= 20 (OpenClaw defaults to Node 24 and still supports Node 22 LTS, currently `22.16+`)
|
||||
- `protobufjs` `postinstall` -- emits warnings about incompatible version schemes (no build artifacts)
|
||||
|
||||
Bun may block dependency lifecycle scripts unless explicitly trusted (`bun pm untrusted` / `bun pm trust`).
|
||||
For this repo, the commonly blocked scripts are not required:
|
||||
|
||||
- `@whiskeysockets/baileys` `preinstall`: checks Node major >= 20 (OpenClaw defaults to Node 24 and still supports Node 22 LTS, currently `22.16+`).
|
||||
- `protobufjs` `postinstall`: emits warnings about incompatible version schemes (no build artifacts).
|
||||
|
||||
If you hit a real runtime issue that requires these scripts, trust them explicitly:
|
||||
If you hit a runtime issue that requires these scripts, trust them explicitly:
|
||||
|
||||
```sh
|
||||
bun pm trust @whiskeysockets/baileys protobufjs
|
||||
@ -56,4 +52,4 @@ bun pm trust @whiskeysockets/baileys protobufjs
|
||||
|
||||
## Caveats
|
||||
|
||||
- Some scripts still hardcode pnpm (e.g. `docs:build`, `ui:*`, `protocol:check`). Run those via pnpm for now.
|
||||
Some scripts still hardcode pnpm (for example `docs:build`, `ui:*`, `protocol:check`). Run those via pnpm for now.
|
||||
|
||||
@ -4,7 +4,8 @@ read_when:
|
||||
- You want to switch between stable/beta/dev
|
||||
- You want to pin a specific version, tag, or SHA
|
||||
- You are tagging or publishing prereleases
|
||||
title: "Development Channels"
|
||||
title: "Release Channels"
|
||||
sidebarTitle: "Release Channels"
|
||||
---
|
||||
|
||||
# Development channels
|
||||
|
||||
129
docs/install/digitalocean.md
Normal file
129
docs/install/digitalocean.md
Normal file
@ -0,0 +1,129 @@
|
||||
---
|
||||
summary: "Host OpenClaw on a DigitalOcean Droplet"
|
||||
read_when:
|
||||
- Setting up OpenClaw on DigitalOcean
|
||||
- Looking for a simple paid VPS for OpenClaw
|
||||
title: "DigitalOcean"
|
||||
---
|
||||
|
||||
# DigitalOcean
|
||||
|
||||
Run a persistent OpenClaw Gateway on a DigitalOcean Droplet.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- DigitalOcean account ([signup](https://cloud.digitalocean.com/registrations/new))
|
||||
- SSH key pair (or willingness to use password auth)
|
||||
- About 20 minutes
|
||||
|
||||
## Setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a Droplet">
|
||||
<Warning>
|
||||
Use a clean base image (Ubuntu 24.04 LTS). Avoid third-party Marketplace 1-click images unless you have reviewed their startup scripts and firewall defaults.
|
||||
</Warning>
|
||||
|
||||
1. Log into [DigitalOcean](https://cloud.digitalocean.com/).
|
||||
2. Click **Create > Droplets**.
|
||||
3. Choose:
|
||||
- **Region:** Closest to you
|
||||
- **Image:** Ubuntu 24.04 LTS
|
||||
- **Size:** Basic, Regular, 1 vCPU / 1 GB RAM / 25 GB SSD
|
||||
- **Authentication:** SSH key (recommended) or password
|
||||
4. Click **Create Droplet** and note the IP address.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Connect and install">
|
||||
```bash
|
||||
ssh root@YOUR_DROPLET_IP
|
||||
|
||||
apt update && apt upgrade -y
|
||||
|
||||
# Install Node.js 24
|
||||
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
|
||||
apt install -y nodejs
|
||||
|
||||
# Install OpenClaw
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash
|
||||
openclaw --version
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Run onboarding">
|
||||
```bash
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
|
||||
The wizard walks you through model auth, channel setup, gateway token generation, and daemon installation (systemd).
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add swap (recommended for 1 GB Droplets)">
|
||||
```bash
|
||||
fallocate -l 2G /swapfile
|
||||
chmod 600 /swapfile
|
||||
mkswap /swapfile
|
||||
swapon /swapfile
|
||||
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Verify the gateway">
|
||||
```bash
|
||||
openclaw status
|
||||
systemctl --user status openclaw-gateway.service
|
||||
journalctl --user -u openclaw-gateway.service -f
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Access the Control UI">
|
||||
The gateway binds to loopback by default. Pick one of these options.
|
||||
|
||||
**Option A: SSH tunnel (simplest)**
|
||||
|
||||
```bash
|
||||
# From your local machine
|
||||
ssh -L 18789:localhost:18789 root@YOUR_DROPLET_IP
|
||||
```
|
||||
|
||||
Then open `http://localhost:18789`.
|
||||
|
||||
**Option B: Tailscale Serve**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
tailscale up
|
||||
openclaw config set gateway.tailscale.mode serve
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
Then open `https://<magicdns>/` from any device on your tailnet.
|
||||
|
||||
**Option C: Tailnet bind (no Serve)**
|
||||
|
||||
```bash
|
||||
openclaw config set gateway.bind tailnet
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
Then open `http://<tailscale-ip>:18789` (token required).
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Gateway will not start** -- Run `openclaw doctor --non-interactive` and check logs with `journalctl --user -u openclaw-gateway.service -n 50`.
|
||||
|
||||
**Port already in use** -- Run `lsof -i :18789` to find the process, then stop it.
|
||||
|
||||
**Out of memory** -- Verify swap is active with `free -h`. If still hitting OOM, use API-based models (Claude, GPT) rather than local models, or upgrade to a 2 GB Droplet.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Channels](/channels) -- connect Telegram, WhatsApp, Discord, and more
|
||||
- [Gateway configuration](/gateway/configuration) -- all config options
|
||||
- [Updating](/install/updating) -- keep OpenClaw up to date
|
||||
@ -71,6 +71,10 @@ ENV NODE_ENV=production
|
||||
CMD ["node","dist/index.js"]
|
||||
```
|
||||
|
||||
<Note>
|
||||
The download URLs above are for x86_64 (amd64). For ARM-based VMs (e.g. Hetzner ARM, GCP Tau T2A), replace the download URLs with the appropriate ARM64 variants from each tool's release page.
|
||||
</Note>
|
||||
|
||||
## Build and launch
|
||||
|
||||
```bash
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -16,9 +16,9 @@ This page assumes exe.dev's default **exeuntu** image. If you picked a different
|
||||
|
||||
1. [https://exe.new/openclaw](https://exe.new/openclaw)
|
||||
2. Fill in your auth key/token as needed
|
||||
3. Click on "Agent" next to your VM, and wait...
|
||||
4. ???
|
||||
5. Profit
|
||||
3. Click on "Agent" next to your VM and wait for Shelley to finish provisioning
|
||||
4. Open `https://<vm-name>.exe.xyz/` and paste your gateway token to authenticate
|
||||
5. Approve any pending device pairing requests with `openclaw devices approve <requestId>`
|
||||
|
||||
## What you need
|
||||
|
||||
|
||||
@ -25,222 +25,228 @@ read_when:
|
||||
3. Deploy with `fly deploy`
|
||||
4. SSH in to create config or use Control UI
|
||||
|
||||
## 1) Create the Fly app
|
||||
<Steps>
|
||||
<Step title="Create the Fly app">
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://github.com/openclaw/openclaw.git
|
||||
cd openclaw
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://github.com/openclaw/openclaw.git
|
||||
cd openclaw
|
||||
# Create a new Fly app (pick your own name)
|
||||
fly apps create my-openclaw
|
||||
|
||||
# Create a new Fly app (pick your own name)
|
||||
fly apps create my-openclaw
|
||||
# Create a persistent volume (1GB is usually enough)
|
||||
fly volumes create openclaw_data --size 1 --region iad
|
||||
```
|
||||
|
||||
# Create a persistent volume (1GB is usually enough)
|
||||
fly volumes create openclaw_data --size 1 --region iad
|
||||
```
|
||||
**Tip:** Choose a region close to you. Common options: `lhr` (London), `iad` (Virginia), `sjc` (San Jose).
|
||||
|
||||
**Tip:** Choose a region close to you. Common options: `lhr` (London), `iad` (Virginia), `sjc` (San Jose).
|
||||
</Step>
|
||||
|
||||
## 2) Configure fly.toml
|
||||
<Step title="Configure fly.toml">
|
||||
Edit `fly.toml` to match your app name and requirements.
|
||||
|
||||
Edit `fly.toml` to match your app name and requirements.
|
||||
**Security note:** The default config exposes a public URL. For a hardened deployment with no public IP, see [Private Deployment](#private-deployment-hardened) or use `fly.private.toml`.
|
||||
|
||||
**Security note:** The default config exposes a public URL. For a hardened deployment with no public IP, see [Private Deployment](#private-deployment-hardened) or use `fly.private.toml`.
|
||||
```toml
|
||||
app = "my-openclaw" # Your app name
|
||||
primary_region = "iad"
|
||||
|
||||
```toml
|
||||
app = "my-openclaw" # Your app name
|
||||
primary_region = "iad"
|
||||
[build]
|
||||
dockerfile = "Dockerfile"
|
||||
|
||||
[build]
|
||||
dockerfile = "Dockerfile"
|
||||
[env]
|
||||
NODE_ENV = "production"
|
||||
OPENCLAW_PREFER_PNPM = "1"
|
||||
OPENCLAW_STATE_DIR = "/data"
|
||||
NODE_OPTIONS = "--max-old-space-size=1536"
|
||||
|
||||
[env]
|
||||
NODE_ENV = "production"
|
||||
OPENCLAW_PREFER_PNPM = "1"
|
||||
OPENCLAW_STATE_DIR = "/data"
|
||||
NODE_OPTIONS = "--max-old-space-size=1536"
|
||||
[processes]
|
||||
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
|
||||
|
||||
[processes]
|
||||
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
|
||||
[http_service]
|
||||
internal_port = 3000
|
||||
force_https = true
|
||||
auto_stop_machines = false
|
||||
auto_start_machines = true
|
||||
min_machines_running = 1
|
||||
processes = ["app"]
|
||||
|
||||
[http_service]
|
||||
internal_port = 3000
|
||||
force_https = true
|
||||
auto_stop_machines = false
|
||||
auto_start_machines = true
|
||||
min_machines_running = 1
|
||||
processes = ["app"]
|
||||
[[vm]]
|
||||
size = "shared-cpu-2x"
|
||||
memory = "2048mb"
|
||||
|
||||
[[vm]]
|
||||
size = "shared-cpu-2x"
|
||||
memory = "2048mb"
|
||||
[mounts]
|
||||
source = "openclaw_data"
|
||||
destination = "/data"
|
||||
```
|
||||
|
||||
[mounts]
|
||||
source = "openclaw_data"
|
||||
destination = "/data"
|
||||
```
|
||||
**Key settings:**
|
||||
|
||||
**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 |
|
||||
</Step>
|
||||
|
||||
## 3) Set secrets
|
||||
<Step title="Set secrets">
|
||||
```bash
|
||||
# Required: Gateway token (for non-loopback binding)
|
||||
fly secrets set OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 32)
|
||||
|
||||
```bash
|
||||
# Required: Gateway token (for non-loopback binding)
|
||||
fly secrets set OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 32)
|
||||
# Model provider API keys
|
||||
fly secrets set ANTHROPIC_API_KEY=sk-ant-...
|
||||
|
||||
# Model provider API keys
|
||||
fly secrets set ANTHROPIC_API_KEY=sk-ant-...
|
||||
# Optional: Other providers
|
||||
fly secrets set OPENAI_API_KEY=sk-...
|
||||
fly secrets set GOOGLE_API_KEY=...
|
||||
|
||||
# Optional: Other providers
|
||||
fly secrets set OPENAI_API_KEY=sk-...
|
||||
fly secrets set GOOGLE_API_KEY=...
|
||||
# Channel tokens
|
||||
fly secrets set DISCORD_BOT_TOKEN=MTQ...
|
||||
```
|
||||
|
||||
# Channel tokens
|
||||
fly secrets set DISCORD_BOT_TOKEN=MTQ...
|
||||
```
|
||||
**Notes:**
|
||||
|
||||
**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.
|
||||
|
||||
- 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.
|
||||
</Step>
|
||||
|
||||
## 4) Deploy
|
||||
<Step title="Deploy">
|
||||
```bash
|
||||
fly deploy
|
||||
```
|
||||
|
||||
```bash
|
||||
fly deploy
|
||||
```
|
||||
First deploy builds the Docker image (~2-3 minutes). Subsequent deploys are faster.
|
||||
|
||||
First deploy builds the Docker image (~2-3 minutes). Subsequent deploys are faster.
|
||||
After deployment, verify:
|
||||
|
||||
After deployment, verify:
|
||||
```bash
|
||||
fly status
|
||||
fly logs
|
||||
```
|
||||
|
||||
```bash
|
||||
fly status
|
||||
fly logs
|
||||
```
|
||||
You should see:
|
||||
|
||||
You should see:
|
||||
```
|
||||
[gateway] listening on ws://0.0.0.0:3000 (PID xxx)
|
||||
[discord] logged in to discord as xxx
|
||||
```
|
||||
|
||||
```
|
||||
[gateway] listening on ws://0.0.0.0:3000 (PID xxx)
|
||||
[discord] logged in to discord as xxx
|
||||
```
|
||||
</Step>
|
||||
|
||||
## 5) Create config file
|
||||
<Step title="Create config file">
|
||||
SSH into the machine to create a proper config:
|
||||
|
||||
SSH into the machine to create a proper config:
|
||||
```bash
|
||||
fly ssh console
|
||||
```
|
||||
|
||||
```bash
|
||||
fly ssh console
|
||||
```
|
||||
Create the config directory and file:
|
||||
|
||||
Create the config directory and file:
|
||||
|
||||
```bash
|
||||
mkdir -p /data
|
||||
cat > /data/openclaw.json << 'EOF'
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "anthropic/claude-opus-4-6",
|
||||
"fallbacks": ["anthropic/claude-sonnet-4-5", "openai/gpt-4o"]
|
||||
},
|
||||
"maxConcurrent": 4
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"auth": {
|
||||
"profiles": {
|
||||
"anthropic:default": { "mode": "token", "provider": "anthropic" },
|
||||
"openai:default": { "mode": "token", "provider": "openai" }
|
||||
}
|
||||
},
|
||||
"bindings": [
|
||||
```bash
|
||||
mkdir -p /data
|
||||
cat > /data/openclaw.json << 'EOF'
|
||||
{
|
||||
"agentId": "main",
|
||||
"match": { "channel": "discord" }
|
||||
}
|
||||
],
|
||||
"channels": {
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"groupPolicy": "allowlist",
|
||||
"guilds": {
|
||||
"YOUR_GUILD_ID": {
|
||||
"channels": { "general": { "allow": true } },
|
||||
"requireMention": false
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "anthropic/claude-opus-4-6",
|
||||
"fallbacks": ["anthropic/claude-sonnet-4-6", "openai/gpt-4o"]
|
||||
},
|
||||
"maxConcurrent": 4
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"auth": {
|
||||
"profiles": {
|
||||
"anthropic:default": { "mode": "token", "provider": "anthropic" },
|
||||
"openai:default": { "mode": "token", "provider": "openai" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"bindings": [
|
||||
{
|
||||
"agentId": "main",
|
||||
"match": { "channel": "discord" }
|
||||
}
|
||||
],
|
||||
"channels": {
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"groupPolicy": "allowlist",
|
||||
"guilds": {
|
||||
"YOUR_GUILD_ID": {
|
||||
"channels": { "general": { "allow": true } },
|
||||
"requireMention": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gateway": {
|
||||
"mode": "local",
|
||||
"bind": "auto"
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
},
|
||||
"gateway": {
|
||||
"mode": "local",
|
||||
"bind": "auto"
|
||||
},
|
||||
"meta": {
|
||||
"lastTouchedVersion": "2026.1.29"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
EOF
|
||||
```
|
||||
|
||||
**Note:** With `OPENCLAW_STATE_DIR=/data`, the config path is `/data/openclaw.json`.
|
||||
**Note:** With `OPENCLAW_STATE_DIR=/data`, the config path is `/data/openclaw.json`.
|
||||
|
||||
**Note:** The Discord token can come from either:
|
||||
**Note:** The Discord token can come from either:
|
||||
|
||||
- Environment variable: `DISCORD_BOT_TOKEN` (recommended for secrets)
|
||||
- Config file: `channels.discord.token`
|
||||
- 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.
|
||||
If using env var, no need to add token to config. The gateway reads `DISCORD_BOT_TOKEN` automatically.
|
||||
|
||||
Restart to apply:
|
||||
Restart to apply:
|
||||
|
||||
```bash
|
||||
exit
|
||||
fly machine restart <machine-id>
|
||||
```
|
||||
```bash
|
||||
exit
|
||||
fly machine restart <machine-id>
|
||||
```
|
||||
|
||||
## 6) Access the Gateway
|
||||
</Step>
|
||||
|
||||
### Control UI
|
||||
<Step title="Access the Gateway">
|
||||
### Control UI
|
||||
|
||||
Open in browser:
|
||||
Open in browser:
|
||||
|
||||
```bash
|
||||
fly open
|
||||
```
|
||||
```bash
|
||||
fly open
|
||||
```
|
||||
|
||||
Or visit `https://my-openclaw.fly.dev/`
|
||||
Or visit `https://my-openclaw.fly.dev/`
|
||||
|
||||
Paste your gateway token (the one from `OPENCLAW_GATEWAY_TOKEN`) to authenticate.
|
||||
Paste your gateway token (the one from `OPENCLAW_GATEWAY_TOKEN`) to authenticate.
|
||||
|
||||
### Logs
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
fly logs # Live logs
|
||||
fly logs --no-tail # Recent logs
|
||||
```
|
||||
```bash
|
||||
fly logs # Live logs
|
||||
fly logs --no-tail # Recent logs
|
||||
```
|
||||
|
||||
### SSH Console
|
||||
### SSH Console
|
||||
|
||||
```bash
|
||||
fly ssh console
|
||||
```
|
||||
```bash
|
||||
fly ssh console
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@ -442,22 +448,22 @@ If you need webhook callbacks (Twilio, Telnyx, etc.) without public exposure:
|
||||
|
||||
Example voice-call config with ngrok:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"provider": "twilio",
|
||||
"tunnel": { "provider": "ngrok" },
|
||||
"webhookSecurity": {
|
||||
"allowedHosts": ["example.ngrok.app"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
enabled: true,
|
||||
config: {
|
||||
provider: "twilio",
|
||||
tunnel: { provider: "ngrok" },
|
||||
webhookSecurity: {
|
||||
allowedHosts: ["example.ngrok.app"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -488,3 +494,9 @@ With the recommended config (`shared-cpu-2x`, 2GB RAM):
|
||||
- Free tier includes some allowance
|
||||
|
||||
See [Fly.io pricing](https://fly.io/docs/about/pricing/) for details.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Set up messaging channels: [Channels](/channels)
|
||||
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
|
||||
- Keep OpenClaw up to date: [Updating](/install/updating)
|
||||
|
||||
@ -65,274 +65,271 @@ For the generic Docker flow, see [Docker](/install/docker).
|
||||
|
||||
---
|
||||
|
||||
## 1) Install gcloud CLI (or use Console)
|
||||
<Steps>
|
||||
<Step title="Install gcloud CLI (or use Console)">
|
||||
**Option A: gcloud CLI** (recommended for automation)
|
||||
|
||||
**Option A: gcloud CLI** (recommended for automation)
|
||||
Install from [https://cloud.google.com/sdk/docs/install](https://cloud.google.com/sdk/docs/install)
|
||||
|
||||
Install from [https://cloud.google.com/sdk/docs/install](https://cloud.google.com/sdk/docs/install)
|
||||
Initialize and authenticate:
|
||||
|
||||
Initialize and authenticate:
|
||||
```bash
|
||||
gcloud init
|
||||
gcloud auth login
|
||||
```
|
||||
|
||||
```bash
|
||||
gcloud init
|
||||
gcloud auth login
|
||||
```
|
||||
**Option B: Cloud Console**
|
||||
|
||||
**Option B: Cloud Console**
|
||||
All steps can be done via the web UI at [https://console.cloud.google.com](https://console.cloud.google.com)
|
||||
|
||||
All steps can be done via the web UI at [https://console.cloud.google.com](https://console.cloud.google.com)
|
||||
</Step>
|
||||
|
||||
---
|
||||
<Step title="Create a GCP project">
|
||||
**CLI:**
|
||||
|
||||
## 2) Create a GCP project
|
||||
```bash
|
||||
gcloud projects create my-openclaw-project --name="OpenClaw Gateway"
|
||||
gcloud config set project my-openclaw-project
|
||||
```
|
||||
|
||||
**CLI:**
|
||||
Enable billing at [https://console.cloud.google.com/billing](https://console.cloud.google.com/billing) (required for Compute Engine).
|
||||
|
||||
```bash
|
||||
gcloud projects create my-openclaw-project --name="OpenClaw Gateway"
|
||||
gcloud config set project my-openclaw-project
|
||||
```
|
||||
Enable the Compute Engine API:
|
||||
|
||||
Enable billing at [https://console.cloud.google.com/billing](https://console.cloud.google.com/billing) (required for Compute Engine).
|
||||
```bash
|
||||
gcloud services enable compute.googleapis.com
|
||||
```
|
||||
|
||||
Enable the Compute Engine API:
|
||||
**Console:**
|
||||
|
||||
```bash
|
||||
gcloud services enable compute.googleapis.com
|
||||
```
|
||||
1. Go to IAM & Admin > Create Project
|
||||
2. Name it and create
|
||||
3. Enable billing for the project
|
||||
4. Navigate to APIs & Services > Enable APIs > search "Compute Engine API" > Enable
|
||||
|
||||
**Console:**
|
||||
</Step>
|
||||
|
||||
1. Go to IAM & Admin > Create Project
|
||||
2. Name it and create
|
||||
3. Enable billing for the project
|
||||
4. Navigate to APIs & Services > Enable APIs > search "Compute Engine API" > Enable
|
||||
<Step title="Create the VM">
|
||||
**Machine types:**
|
||||
|
||||
---
|
||||
| Type | Specs | Cost | Notes |
|
||||
| --------- | ------------------------ | ------------------ | -------------------------------------------- |
|
||||
| e2-medium | 2 vCPU, 4GB RAM | ~$25/mo | Most reliable for local Docker builds |
|
||||
| e2-small | 2 vCPU, 2GB RAM | ~$12/mo | Minimum recommended for Docker build |
|
||||
| e2-micro | 2 vCPU (shared), 1GB RAM | Free tier eligible | Often fails with Docker build OOM (exit 137) |
|
||||
|
||||
## 3) Create the VM
|
||||
**CLI:**
|
||||
|
||||
**Machine types:**
|
||||
```bash
|
||||
gcloud compute instances create openclaw-gateway \
|
||||
--zone=us-central1-a \
|
||||
--machine-type=e2-small \
|
||||
--boot-disk-size=20GB \
|
||||
--image-family=debian-12 \
|
||||
--image-project=debian-cloud
|
||||
```
|
||||
|
||||
| Type | Specs | Cost | Notes |
|
||||
| --------- | ------------------------ | ------------------ | -------------------------------------------- |
|
||||
| e2-medium | 2 vCPU, 4GB RAM | ~$25/mo | Most reliable for local Docker builds |
|
||||
| e2-small | 2 vCPU, 2GB RAM | ~$12/mo | Minimum recommended for Docker build |
|
||||
| e2-micro | 2 vCPU (shared), 1GB RAM | Free tier eligible | Often fails with Docker build OOM (exit 137) |
|
||||
**Console:**
|
||||
|
||||
**CLI:**
|
||||
1. Go to Compute Engine > VM instances > Create instance
|
||||
2. Name: `openclaw-gateway`
|
||||
3. Region: `us-central1`, Zone: `us-central1-a`
|
||||
4. Machine type: `e2-small`
|
||||
5. Boot disk: Debian 12, 20GB
|
||||
6. Create
|
||||
|
||||
```bash
|
||||
gcloud compute instances create openclaw-gateway \
|
||||
--zone=us-central1-a \
|
||||
--machine-type=e2-small \
|
||||
--boot-disk-size=20GB \
|
||||
--image-family=debian-12 \
|
||||
--image-project=debian-cloud
|
||||
```
|
||||
</Step>
|
||||
|
||||
**Console:**
|
||||
<Step title="SSH into the VM">
|
||||
**CLI:**
|
||||
|
||||
1. Go to Compute Engine > VM instances > Create instance
|
||||
2. Name: `openclaw-gateway`
|
||||
3. Region: `us-central1`, Zone: `us-central1-a`
|
||||
4. Machine type: `e2-small`
|
||||
5. Boot disk: Debian 12, 20GB
|
||||
6. Create
|
||||
```bash
|
||||
gcloud compute ssh openclaw-gateway --zone=us-central1-a
|
||||
```
|
||||
|
||||
---
|
||||
**Console:**
|
||||
|
||||
## 4) SSH into the VM
|
||||
Click the "SSH" button next to your VM in the Compute Engine dashboard.
|
||||
|
||||
**CLI:**
|
||||
Note: SSH key propagation can take 1-2 minutes after VM creation. If connection is refused, wait and retry.
|
||||
|
||||
```bash
|
||||
gcloud compute ssh openclaw-gateway --zone=us-central1-a
|
||||
```
|
||||
</Step>
|
||||
|
||||
**Console:**
|
||||
<Step title="Install Docker (on the VM)">
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git curl ca-certificates
|
||||
curl -fsSL https://get.docker.com | sudo sh
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
Click the "SSH" button next to your VM in the Compute Engine dashboard.
|
||||
Log out and back in for the group change to take effect:
|
||||
|
||||
Note: SSH key propagation can take 1-2 minutes after VM creation. If connection is refused, wait and retry.
|
||||
```bash
|
||||
exit
|
||||
```
|
||||
|
||||
---
|
||||
Then SSH back in:
|
||||
|
||||
## 5) Install Docker (on the VM)
|
||||
```bash
|
||||
gcloud compute ssh openclaw-gateway --zone=us-central1-a
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git curl ca-certificates
|
||||
curl -fsSL https://get.docker.com | sudo sh
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
Verify:
|
||||
|
||||
Log out and back in for the group change to take effect:
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
```bash
|
||||
exit
|
||||
```
|
||||
</Step>
|
||||
|
||||
Then SSH back in:
|
||||
<Step title="Clone the OpenClaw repository">
|
||||
```bash
|
||||
git clone https://github.com/openclaw/openclaw.git
|
||||
cd openclaw
|
||||
```
|
||||
|
||||
```bash
|
||||
gcloud compute ssh openclaw-gateway --zone=us-central1-a
|
||||
```
|
||||
This guide assumes you will build a custom image to guarantee binary persistence.
|
||||
|
||||
Verify:
|
||||
</Step>
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
<Step title="Create persistent host directories">
|
||||
Docker containers are ephemeral.
|
||||
All long-lived state must live on the host.
|
||||
|
||||
---
|
||||
```bash
|
||||
mkdir -p ~/.openclaw
|
||||
mkdir -p ~/.openclaw/workspace
|
||||
```
|
||||
|
||||
## 6) Clone the OpenClaw repository
|
||||
</Step>
|
||||
|
||||
```bash
|
||||
git clone https://github.com/openclaw/openclaw.git
|
||||
cd openclaw
|
||||
```
|
||||
<Step title="Configure environment variables">
|
||||
Create `.env` in the repository root.
|
||||
|
||||
This guide assumes you will build a custom image to guarantee binary persistence.
|
||||
```bash
|
||||
OPENCLAW_IMAGE=openclaw:latest
|
||||
OPENCLAW_GATEWAY_TOKEN=change-me-now
|
||||
OPENCLAW_GATEWAY_BIND=lan
|
||||
OPENCLAW_GATEWAY_PORT=18789
|
||||
|
||||
---
|
||||
OPENCLAW_CONFIG_DIR=/home/$USER/.openclaw
|
||||
OPENCLAW_WORKSPACE_DIR=/home/$USER/.openclaw/workspace
|
||||
|
||||
## 7) Create persistent host directories
|
||||
GOG_KEYRING_PASSWORD=change-me-now
|
||||
XDG_CONFIG_HOME=/home/node/.openclaw
|
||||
```
|
||||
|
||||
Docker containers are ephemeral.
|
||||
All long-lived state must live on the host.
|
||||
Generate strong secrets:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.openclaw
|
||||
mkdir -p ~/.openclaw/workspace
|
||||
```
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
---
|
||||
**Do not commit this file.**
|
||||
|
||||
## 8) Configure environment variables
|
||||
</Step>
|
||||
|
||||
Create `.env` in the repository root.
|
||||
<Step title="Docker Compose configuration">
|
||||
Create or update `docker-compose.yml`.
|
||||
|
||||
```bash
|
||||
OPENCLAW_IMAGE=openclaw:latest
|
||||
OPENCLAW_GATEWAY_TOKEN=change-me-now
|
||||
OPENCLAW_GATEWAY_BIND=lan
|
||||
OPENCLAW_GATEWAY_PORT=18789
|
||||
```yaml
|
||||
services:
|
||||
openclaw-gateway:
|
||||
image: ${OPENCLAW_IMAGE}
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- HOME=/home/node
|
||||
- NODE_ENV=production
|
||||
- TERM=xterm-256color
|
||||
- OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND}
|
||||
- OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT}
|
||||
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
|
||||
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
|
||||
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
|
||||
- PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
volumes:
|
||||
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
|
||||
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
|
||||
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"
|
||||
command:
|
||||
[
|
||||
"node",
|
||||
"dist/index.js",
|
||||
"gateway",
|
||||
"--bind",
|
||||
"${OPENCLAW_GATEWAY_BIND}",
|
||||
"--port",
|
||||
"${OPENCLAW_GATEWAY_PORT}",
|
||||
"--allow-unconfigured",
|
||||
]
|
||||
```
|
||||
|
||||
OPENCLAW_CONFIG_DIR=/home/$USER/.openclaw
|
||||
OPENCLAW_WORKSPACE_DIR=/home/$USER/.openclaw/workspace
|
||||
`--allow-unconfigured` is only for bootstrap convenience, it is not a replacement for a proper gateway configuration. Still set auth (`gateway.auth.token` or password) and use safe bind settings for your deployment.
|
||||
|
||||
GOG_KEYRING_PASSWORD=change-me-now
|
||||
XDG_CONFIG_HOME=/home/node/.openclaw
|
||||
```
|
||||
</Step>
|
||||
|
||||
Generate strong secrets:
|
||||
<Step title="Shared Docker VM runtime steps">
|
||||
Use the shared runtime guide for the common Docker host flow:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
- [Bake required binaries into the image](/install/docker-vm-runtime#bake-required-binaries-into-the-image)
|
||||
- [Build and launch](/install/docker-vm-runtime#build-and-launch)
|
||||
- [What persists where](/install/docker-vm-runtime#what-persists-where)
|
||||
- [Updates](/install/docker-vm-runtime#updates)
|
||||
|
||||
**Do not commit this file.**
|
||||
</Step>
|
||||
|
||||
---
|
||||
<Step title="GCP-specific launch notes">
|
||||
On GCP, if build fails with `Killed` or `exit code 137` during `pnpm install --frozen-lockfile`, the VM is out of memory. Use `e2-small` minimum, or `e2-medium` for more reliable first builds.
|
||||
|
||||
## 9) Docker Compose configuration
|
||||
When binding to LAN (`OPENCLAW_GATEWAY_BIND=lan`), configure a trusted browser origin before continuing:
|
||||
|
||||
Create or update `docker-compose.yml`.
|
||||
```bash
|
||||
docker compose run --rm openclaw-cli config set gateway.controlUi.allowedOrigins '["http://127.0.0.1:18789"]' --strict-json
|
||||
```
|
||||
|
||||
If you changed the gateway port, replace `18789` with your configured port.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
openclaw-gateway:
|
||||
image: ${OPENCLAW_IMAGE}
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- HOME=/home/node
|
||||
- NODE_ENV=production
|
||||
- TERM=xterm-256color
|
||||
- OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND}
|
||||
- OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT}
|
||||
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
|
||||
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
|
||||
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
|
||||
- PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
volumes:
|
||||
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
|
||||
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
|
||||
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"
|
||||
command:
|
||||
[
|
||||
"node",
|
||||
"dist/index.js",
|
||||
"gateway",
|
||||
"--bind",
|
||||
"${OPENCLAW_GATEWAY_BIND}",
|
||||
"--port",
|
||||
"${OPENCLAW_GATEWAY_PORT}",
|
||||
]
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Access from your laptop">
|
||||
Create an SSH tunnel to forward the Gateway port:
|
||||
|
||||
---
|
||||
```bash
|
||||
gcloud compute ssh openclaw-gateway --zone=us-central1-a -- -L 18789:127.0.0.1:18789
|
||||
```
|
||||
|
||||
Open in your browser:
|
||||
|
||||
## 10) Shared Docker VM runtime steps
|
||||
`http://127.0.0.1:18789/`
|
||||
|
||||
Fetch a fresh tokenized dashboard link:
|
||||
|
||||
Use the shared runtime guide for the common Docker host flow:
|
||||
```bash
|
||||
docker compose run --rm openclaw-cli dashboard --no-open
|
||||
```
|
||||
|
||||
- [Bake required binaries into the image](/install/docker-vm-runtime#bake-required-binaries-into-the-image)
|
||||
- [Build and launch](/install/docker-vm-runtime#build-and-launch)
|
||||
- [What persists where](/install/docker-vm-runtime#what-persists-where)
|
||||
- [Updates](/install/docker-vm-runtime#updates)
|
||||
Paste the token from that URL.
|
||||
|
||||
If Control UI shows `unauthorized` or `disconnected (1008): pairing required`, approve the browser device:
|
||||
|
||||
---
|
||||
|
||||
## 11) GCP-specific launch notes
|
||||
|
||||
On GCP, if build fails with `Killed` or `exit code 137` during `pnpm install --frozen-lockfile`, the VM is out of memory. Use `e2-small` minimum, or `e2-medium` for more reliable first builds.
|
||||
|
||||
When binding to LAN (`OPENCLAW_GATEWAY_BIND=lan`), configure a trusted browser origin before continuing:
|
||||
|
||||
```bash
|
||||
docker compose run --rm openclaw-cli config set gateway.controlUi.allowedOrigins '["http://127.0.0.1:18789"]' --strict-json
|
||||
```
|
||||
|
||||
If you changed the gateway port, replace `18789` with your configured port.
|
||||
|
||||
## 12) Access from your laptop
|
||||
|
||||
Create an SSH tunnel to forward the Gateway port:
|
||||
|
||||
```bash
|
||||
gcloud compute ssh openclaw-gateway --zone=us-central1-a -- -L 18789:127.0.0.1:18789
|
||||
```
|
||||
|
||||
Open in your browser:
|
||||
|
||||
`http://127.0.0.1:18789/`
|
||||
|
||||
Fetch a fresh tokenized dashboard link:
|
||||
|
||||
```bash
|
||||
docker compose run --rm openclaw-cli dashboard --no-open
|
||||
```
|
||||
|
||||
Paste the token from that URL.
|
||||
|
||||
If Control UI shows `unauthorized` or `disconnected (1008): pairing required`, approve the browser device:
|
||||
|
||||
```bash
|
||||
docker compose run --rm openclaw-cli devices list
|
||||
docker compose run --rm openclaw-cli devices approve <requestId>
|
||||
```
|
||||
|
||||
Need the shared persistence and update reference again?
|
||||
See [Docker VM Runtime](/install/docker-vm-runtime#what-persists-where) and [Docker VM Runtime updates](/install/docker-vm-runtime#updates).
|
||||
```bash
|
||||
docker compose run --rm openclaw-cli devices list
|
||||
docker compose run --rm openclaw-cli devices approve <requestId>
|
||||
```
|
||||
|
||||
Need the shared persistence and update reference again?
|
||||
See [Docker VM Runtime](/install/docker-vm-runtime#what-persists-where) and [Docker VM Runtime updates](/install/docker-vm-runtime#updates).
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -72,162 +72,156 @@ For the generic Docker flow, see [Docker](/install/docker).
|
||||
|
||||
---
|
||||
|
||||
## 1) Provision the VPS
|
||||
<Steps>
|
||||
<Step title="Provision the VPS">
|
||||
Create an Ubuntu or Debian VPS in Hetzner.
|
||||
|
||||
Create an Ubuntu or Debian VPS in Hetzner.
|
||||
Connect as root:
|
||||
|
||||
Connect as root:
|
||||
```bash
|
||||
ssh root@YOUR_VPS_IP
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@YOUR_VPS_IP
|
||||
```
|
||||
This guide assumes the VPS is stateful.
|
||||
Do not treat it as disposable infrastructure.
|
||||
|
||||
This guide assumes the VPS is stateful.
|
||||
Do not treat it as disposable infrastructure.
|
||||
</Step>
|
||||
|
||||
---
|
||||
<Step title="Install Docker (on the VPS)">
|
||||
```bash
|
||||
apt-get update
|
||||
apt-get install -y git curl ca-certificates
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
```
|
||||
|
||||
## 2) Install Docker (on the VPS)
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
apt-get update
|
||||
apt-get install -y git curl ca-certificates
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
```
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
Verify:
|
||||
</Step>
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
<Step title="Clone the OpenClaw repository">
|
||||
```bash
|
||||
git clone https://github.com/openclaw/openclaw.git
|
||||
cd openclaw
|
||||
```
|
||||
|
||||
---
|
||||
This guide assumes you will build a custom image to guarantee binary persistence.
|
||||
|
||||
## 3) Clone the OpenClaw repository
|
||||
</Step>
|
||||
|
||||
```bash
|
||||
git clone https://github.com/openclaw/openclaw.git
|
||||
cd openclaw
|
||||
```
|
||||
<Step title="Create persistent host directories">
|
||||
Docker containers are ephemeral.
|
||||
All long-lived state must live on the host.
|
||||
|
||||
This guide assumes you will build a custom image to guarantee binary persistence.
|
||||
```bash
|
||||
mkdir -p /root/.openclaw/workspace
|
||||
|
||||
---
|
||||
# Set ownership to the container user (uid 1000):
|
||||
chown -R 1000:1000 /root/.openclaw
|
||||
```
|
||||
|
||||
## 4) Create persistent host directories
|
||||
</Step>
|
||||
|
||||
Docker containers are ephemeral.
|
||||
All long-lived state must live on the host.
|
||||
<Step title="Configure environment variables">
|
||||
Create `.env` in the repository root.
|
||||
|
||||
```bash
|
||||
mkdir -p /root/.openclaw/workspace
|
||||
```bash
|
||||
OPENCLAW_IMAGE=openclaw:latest
|
||||
OPENCLAW_GATEWAY_TOKEN=change-me-now
|
||||
OPENCLAW_GATEWAY_BIND=lan
|
||||
OPENCLAW_GATEWAY_PORT=18789
|
||||
|
||||
# Set ownership to the container user (uid 1000):
|
||||
chown -R 1000:1000 /root/.openclaw
|
||||
```
|
||||
OPENCLAW_CONFIG_DIR=/root/.openclaw
|
||||
OPENCLAW_WORKSPACE_DIR=/root/.openclaw/workspace
|
||||
|
||||
---
|
||||
GOG_KEYRING_PASSWORD=change-me-now
|
||||
XDG_CONFIG_HOME=/home/node/.openclaw
|
||||
```
|
||||
|
||||
## 5) Configure environment variables
|
||||
Generate strong secrets:
|
||||
|
||||
Create `.env` in the repository root.
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
```bash
|
||||
OPENCLAW_IMAGE=openclaw:latest
|
||||
OPENCLAW_GATEWAY_TOKEN=change-me-now
|
||||
OPENCLAW_GATEWAY_BIND=lan
|
||||
OPENCLAW_GATEWAY_PORT=18789
|
||||
**Do not commit this file.**
|
||||
|
||||
OPENCLAW_CONFIG_DIR=/root/.openclaw
|
||||
OPENCLAW_WORKSPACE_DIR=/root/.openclaw/workspace
|
||||
</Step>
|
||||
|
||||
GOG_KEYRING_PASSWORD=change-me-now
|
||||
XDG_CONFIG_HOME=/home/node/.openclaw
|
||||
```
|
||||
<Step title="Docker Compose configuration">
|
||||
Create or update `docker-compose.yml`.
|
||||
|
||||
Generate strong secrets:
|
||||
```yaml
|
||||
services:
|
||||
openclaw-gateway:
|
||||
image: ${OPENCLAW_IMAGE}
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- HOME=/home/node
|
||||
- NODE_ENV=production
|
||||
- TERM=xterm-256color
|
||||
- OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND}
|
||||
- OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT}
|
||||
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
|
||||
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
|
||||
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
|
||||
- PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
volumes:
|
||||
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
|
||||
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
|
||||
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"
|
||||
command:
|
||||
[
|
||||
"node",
|
||||
"dist/index.js",
|
||||
"gateway",
|
||||
"--bind",
|
||||
"${OPENCLAW_GATEWAY_BIND}",
|
||||
"--port",
|
||||
"${OPENCLAW_GATEWAY_PORT}",
|
||||
"--allow-unconfigured",
|
||||
]
|
||||
```
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
`--allow-unconfigured` is only for bootstrap convenience, it is not a replacement for a proper gateway configuration. Still set auth (`gateway.auth.token` or password) and use safe bind settings for your deployment.
|
||||
|
||||
**Do not commit this file.**
|
||||
</Step>
|
||||
|
||||
---
|
||||
<Step title="Shared Docker VM runtime steps">
|
||||
Use the shared runtime guide for the common Docker host flow:
|
||||
|
||||
## 6) Docker Compose configuration
|
||||
- [Bake required binaries into the image](/install/docker-vm-runtime#bake-required-binaries-into-the-image)
|
||||
- [Build and launch](/install/docker-vm-runtime#build-and-launch)
|
||||
- [What persists where](/install/docker-vm-runtime#what-persists-where)
|
||||
- [Updates](/install/docker-vm-runtime#updates)
|
||||
|
||||
Create or update `docker-compose.yml`.
|
||||
</Step>
|
||||
|
||||
```yaml
|
||||
services:
|
||||
openclaw-gateway:
|
||||
image: ${OPENCLAW_IMAGE}
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- HOME=/home/node
|
||||
- NODE_ENV=production
|
||||
- TERM=xterm-256color
|
||||
- OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND}
|
||||
- OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT}
|
||||
- OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN}
|
||||
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
|
||||
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
|
||||
- PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
volumes:
|
||||
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
|
||||
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
|
||||
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"
|
||||
command:
|
||||
[
|
||||
"node",
|
||||
"dist/index.js",
|
||||
"gateway",
|
||||
"--bind",
|
||||
"${OPENCLAW_GATEWAY_BIND}",
|
||||
"--port",
|
||||
"${OPENCLAW_GATEWAY_PORT}",
|
||||
"--allow-unconfigured",
|
||||
]
|
||||
```
|
||||
<Step title="Hetzner-specific access">
|
||||
After the shared build and launch steps, tunnel from your laptop:
|
||||
|
||||
`--allow-unconfigured` is only for bootstrap convenience, it is not a replacement for a proper gateway configuration. Still set auth (`gateway.auth.token` or password) and use safe bind settings for your deployment.
|
||||
```bash
|
||||
ssh -N -L 18789:127.0.0.1:18789 root@YOUR_VPS_IP
|
||||
```
|
||||
|
||||
---
|
||||
Open:
|
||||
|
||||
## 7) Shared Docker VM runtime steps
|
||||
`http://127.0.0.1:18789/`
|
||||
|
||||
Use the shared runtime guide for the common Docker host flow:
|
||||
Paste your gateway token.
|
||||
|
||||
- [Bake required binaries into the image](/install/docker-vm-runtime#bake-required-binaries-into-the-image)
|
||||
- [Build and launch](/install/docker-vm-runtime#build-and-launch)
|
||||
- [What persists where](/install/docker-vm-runtime#what-persists-where)
|
||||
- [Updates](/install/docker-vm-runtime#updates)
|
||||
|
||||
---
|
||||
|
||||
## 8) Hetzner-specific access
|
||||
|
||||
After the shared build and launch steps, tunnel from your laptop:
|
||||
|
||||
```bash
|
||||
ssh -N -L 18789:127.0.0.1:18789 root@YOUR_VPS_IP
|
||||
```
|
||||
|
||||
Open:
|
||||
|
||||
`http://127.0.0.1:18789/`
|
||||
|
||||
Paste your gateway token.
|
||||
|
||||
---
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
The shared persistence map lives in [Docker VM Runtime](/install/docker-vm-runtime#what-persists-where).
|
||||
|
||||
@ -249,3 +243,9 @@ For teams preferring infrastructure-as-code workflows, a community-maintained Te
|
||||
This approach complements the Docker setup above with reproducible deployments, version-controlled infrastructure, and automated disaster recovery.
|
||||
|
||||
> **Note:** Community-maintained. For issues or contributions, see the repository links above.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Set up messaging channels: [Channels](/channels)
|
||||
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
|
||||
- Keep OpenClaw up to date: [Updating](/install/updating)
|
||||
|
||||
@ -9,158 +9,113 @@ title: "Install"
|
||||
|
||||
# Install
|
||||
|
||||
Already followed [Getting Started](/start/getting-started)? You're all set — this page is for alternative install methods, platform-specific instructions, and maintenance.
|
||||
## Recommended: installer script
|
||||
|
||||
The fastest way to install. It detects your OS, installs Node if needed, installs OpenClaw, and launches onboarding.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="macOS / Linux / WSL2">
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Windows (PowerShell)">
|
||||
```powershell
|
||||
iwr -useb https://openclaw.ai/install.ps1 | iex
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
To install without running onboarding:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="macOS / Linux / WSL2">
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Windows (PowerShell)">
|
||||
```powershell
|
||||
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -NoOnboard
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
For all flags and CI/automation options, see [Installer internals](/install/installer).
|
||||
|
||||
## System requirements
|
||||
|
||||
- **[Node 24 (recommended)](/install/node)** (Node 22 LTS, currently `22.16+`, is still supported for compatibility; the [installer script](#install-methods) will install Node 24 if missing)
|
||||
- macOS, Linux, or Windows
|
||||
- `pnpm` only if you build from source
|
||||
- **Node 24** (recommended) or Node 22.16+ — the installer script handles this automatically
|
||||
- **macOS, Linux, or Windows** — both native Windows and WSL2 are supported; WSL2 is more stable. See [Windows](/platforms/windows).
|
||||
- `pnpm` is only needed if you build from source
|
||||
|
||||
<Note>
|
||||
On Windows, we strongly recommend running OpenClaw under [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
</Note>
|
||||
## Alternative install methods
|
||||
|
||||
## Install methods
|
||||
### npm or pnpm
|
||||
|
||||
<Tip>
|
||||
The **installer script** is the recommended way to install OpenClaw. It handles Node detection, installation, and onboarding in one step.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
For VPS/cloud hosts, avoid third-party "1-click" marketplace images when possible. Prefer a clean base OS image (for example Ubuntu LTS), then install OpenClaw yourself with the installer script.
|
||||
</Warning>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Installer script" icon="rocket" defaultOpen>
|
||||
Downloads the CLI, installs it globally via npm, and launches onboarding.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="macOS / Linux / WSL2">
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Windows (PowerShell)">
|
||||
```powershell
|
||||
iwr -useb https://openclaw.ai/install.ps1 | iex
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
That's it — the script handles Node detection, installation, and onboarding.
|
||||
|
||||
To skip onboarding and just install the binary:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="macOS / Linux / WSL2">
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Windows (PowerShell)">
|
||||
```powershell
|
||||
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -NoOnboard
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
For all flags, env vars, and CI/automation options, see [Installer internals](/install/installer).
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="npm / pnpm" icon="package">
|
||||
If you already manage Node yourself, we recommend Node 24. OpenClaw still supports Node 22 LTS, currently `22.16+`, for compatibility:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="npm">
|
||||
```bash
|
||||
npm install -g openclaw@latest
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
|
||||
<Accordion title="sharp build errors?">
|
||||
If you have libvips installed globally (common on macOS via Homebrew) and `sharp` fails, force prebuilt binaries:
|
||||
|
||||
```bash
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest
|
||||
```
|
||||
|
||||
If you see `sharp: Please add node-gyp to your dependencies`, either install build tooling (macOS: Xcode CLT + `npm install -g node-gyp`) or use the env var above.
|
||||
</Accordion>
|
||||
</Tab>
|
||||
<Tab title="pnpm">
|
||||
```bash
|
||||
pnpm add -g openclaw@latest
|
||||
pnpm approve-builds -g # approve openclaw, node-llama-cpp, sharp, etc.
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
|
||||
<Note>
|
||||
pnpm requires explicit approval for packages with build scripts. After the first install shows the "Ignored build scripts" warning, run `pnpm approve-builds -g` and select the listed packages.
|
||||
</Note>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Want the current GitHub `main` head with a package-manager install?
|
||||
If you already manage Node yourself:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="npm">
|
||||
```bash
|
||||
npm install -g github:openclaw/openclaw#main
|
||||
npm install -g openclaw@latest
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="pnpm">
|
||||
```bash
|
||||
pnpm add -g openclaw@latest
|
||||
pnpm approve-builds -g
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm add -g github:openclaw/openclaw#main
|
||||
```
|
||||
<Note>
|
||||
pnpm requires explicit approval for packages with build scripts. Run `pnpm approve-builds -g` after the first install.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Accordion title="From source" icon="github">
|
||||
For contributors or anyone who wants to run from a local checkout.
|
||||
<Accordion title="Troubleshooting: sharp build errors (npm)">
|
||||
If `sharp` fails due to a globally installed libvips:
|
||||
|
||||
<Steps>
|
||||
<Step title="Clone and build">
|
||||
Clone the [OpenClaw repo](https://github.com/openclaw/openclaw) and build:
|
||||
```bash
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone https://github.com/openclaw/openclaw.git
|
||||
cd openclaw
|
||||
pnpm install
|
||||
pnpm ui:build
|
||||
pnpm build
|
||||
```
|
||||
</Step>
|
||||
<Step title="Link the CLI">
|
||||
Make the `openclaw` command available globally:
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
pnpm link --global
|
||||
```
|
||||
### From source
|
||||
|
||||
Alternatively, skip the link and run commands via `pnpm openclaw ...` from inside the repo.
|
||||
</Step>
|
||||
<Step title="Run onboarding">
|
||||
```bash
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
For contributors or anyone who wants to run from a local checkout:
|
||||
|
||||
For deeper development workflows, see [Setup](/start/setup).
|
||||
```bash
|
||||
git clone https://github.com/openclaw/openclaw.git
|
||||
cd openclaw
|
||||
pnpm install && pnpm ui:build && pnpm build
|
||||
pnpm link --global
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
Or skip the link and use `pnpm openclaw ...` from inside the repo. See [Setup](/start/setup) for full development workflows.
|
||||
|
||||
## Other install methods
|
||||
### Install from GitHub main
|
||||
|
||||
```bash
|
||||
npm install -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
### Containers and package managers
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Docker" href="/install/docker" icon="container">
|
||||
Containerized or headless deployments.
|
||||
</Card>
|
||||
<Card title="Podman" href="/install/podman" icon="container">
|
||||
Rootless container: run `setup-podman.sh` once, then the launch script.
|
||||
Rootless container alternative to Docker.
|
||||
</Card>
|
||||
<Card title="Nix" href="/install/nix" icon="snowflake">
|
||||
Declarative install via Nix.
|
||||
Declarative install via Nix flake.
|
||||
</Card>
|
||||
<Card title="Ansible" href="/install/ansible" icon="server">
|
||||
Automated fleet provisioning.
|
||||
@ -170,50 +125,32 @@ For VPS/cloud hosts, avoid third-party "1-click" marketplace images when possibl
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## After install
|
||||
|
||||
Verify everything is working:
|
||||
## Verify the install
|
||||
|
||||
```bash
|
||||
openclaw --version # confirm the CLI is available
|
||||
openclaw doctor # check for config issues
|
||||
openclaw status # gateway status
|
||||
openclaw dashboard # open the browser UI
|
||||
openclaw gateway status # verify the Gateway is running
|
||||
```
|
||||
|
||||
If you need custom runtime paths, use:
|
||||
## Hosting and deployment
|
||||
|
||||
- `OPENCLAW_HOME` for home-directory based internal paths
|
||||
- `OPENCLAW_STATE_DIR` for mutable state location
|
||||
- `OPENCLAW_CONFIG_PATH` for config file location
|
||||
Deploy OpenClaw on a cloud server or VPS:
|
||||
|
||||
See [Environment vars](/help/environment) for precedence and full details.
|
||||
<CardGroup cols={3}>
|
||||
<Card title="VPS" href="/vps">Any Linux VPS</Card>
|
||||
<Card title="Docker VM" href="/install/docker-vm-runtime">Shared Docker steps</Card>
|
||||
<Card title="Kubernetes" href="/install/kubernetes">K8s</Card>
|
||||
<Card title="Fly.io" href="/install/fly">Fly.io</Card>
|
||||
<Card title="Hetzner" href="/install/hetzner">Hetzner</Card>
|
||||
<Card title="GCP" href="/install/gcp">Google Cloud</Card>
|
||||
<Card title="Azure" href="/install/azure">Azure</Card>
|
||||
<Card title="Railway" href="/install/railway">Railway</Card>
|
||||
<Card title="Render" href="/install/render">Render</Card>
|
||||
<Card title="Northflank" href="/install/northflank">Northflank</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Troubleshooting: `openclaw` not found
|
||||
|
||||
<Accordion title="PATH diagnosis and fix">
|
||||
Quick diagnosis:
|
||||
|
||||
```bash
|
||||
node -v
|
||||
npm -v
|
||||
npm prefix -g
|
||||
echo "$PATH"
|
||||
```
|
||||
|
||||
If `$(npm prefix -g)/bin` (macOS/Linux) or `$(npm prefix -g)` (Windows) is **not** in your `$PATH`, your shell can't find global npm binaries (including `openclaw`).
|
||||
|
||||
Fix — add it to your shell startup file (`~/.zshrc` or `~/.bashrc`):
|
||||
|
||||
```bash
|
||||
export PATH="$(npm prefix -g)/bin:$PATH"
|
||||
```
|
||||
|
||||
On Windows, add the output of `npm prefix -g` to your PATH.
|
||||
|
||||
Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
|
||||
</Accordion>
|
||||
|
||||
## Update / uninstall
|
||||
## Update, migrate, or uninstall
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Updating" href="/install/updating" icon="refresh-cw">
|
||||
@ -226,3 +163,21 @@ Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
|
||||
Remove OpenClaw completely.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Troubleshooting: `openclaw` not found
|
||||
|
||||
If the install succeeded but `openclaw` is not found in your terminal:
|
||||
|
||||
```bash
|
||||
node -v # Node installed?
|
||||
npm prefix -g # Where are global packages?
|
||||
echo "$PATH" # Is the global bin dir in PATH?
|
||||
```
|
||||
|
||||
If `$(npm prefix -g)/bin` is not in your `$PATH`, add it to your shell startup file (`~/.zshrc` or `~/.bashrc`):
|
||||
|
||||
```bash
|
||||
export PATH="$(npm prefix -g)/bin:$PATH"
|
||||
```
|
||||
|
||||
Then open a new terminal. See [Node setup](/install/node) for more details.
|
||||
|
||||
@ -180,7 +180,7 @@ Designed for environments where you want everything under a local prefix (defaul
|
||||
|
||||
<Steps>
|
||||
<Step title="Install local Node runtime">
|
||||
Downloads a pinned supported Node tarball (currently default `22.22.0`) to `<prefix>/tools/node-v<version>` and verifies SHA-256.
|
||||
Downloads a pinned supported Node LTS tarball (the version is embedded in the script and updated independently) to `<prefix>/tools/node-v<version>` and verifies SHA-256.
|
||||
</Step>
|
||||
<Step title="Ensure Git">
|
||||
If Git is missing, attempts install via apt/dnf/yum on Linux or Homebrew on macOS.
|
||||
|
||||
@ -138,7 +138,7 @@ OPENCLAW_NAMESPACE=my-namespace ./scripts/k8s/deploy.sh
|
||||
Edit the `image` field in `scripts/k8s/manifests/deployment.yaml`:
|
||||
|
||||
```yaml
|
||||
image: ghcr.io/openclaw/openclaw:2026.3.1
|
||||
image: ghcr.io/openclaw/openclaw:latest # or pin to a specific version from https://github.com/openclaw/openclaw/releases
|
||||
```
|
||||
|
||||
### Expose beyond port-forward
|
||||
|
||||
@ -155,17 +155,17 @@ nano ~/.openclaw/openclaw.json
|
||||
|
||||
Add your channels:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"channels": {
|
||||
"whatsapp": {
|
||||
"dmPolicy": "allowlist",
|
||||
"allowFrom": ["+15551234567"]
|
||||
channels: {
|
||||
whatsapp: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15551234567"],
|
||||
},
|
||||
"telegram": {
|
||||
"botToken": "YOUR_BOT_TOKEN"
|
||||
}
|
||||
}
|
||||
telegram: {
|
||||
botToken: "YOUR_BOT_TOKEN",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -209,15 +209,15 @@ Inside the VM:
|
||||
|
||||
Add to your OpenClaw config:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"channels": {
|
||||
"bluebubbles": {
|
||||
"serverUrl": "http://localhost:1234",
|
||||
"password": "your-api-password",
|
||||
"webhookPath": "/bluebubbles-webhook"
|
||||
}
|
||||
}
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "your-api-password",
|
||||
webhookPath: "/bluebubbles-webhook",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -6,187 +6,105 @@ read_when:
|
||||
title: "Migration Guide"
|
||||
---
|
||||
|
||||
# Migrating OpenClaw to a new machine
|
||||
# Migrating OpenClaw to a New Machine
|
||||
|
||||
This guide migrates an OpenClaw Gateway from one machine to another **without redoing onboarding**.
|
||||
This guide moves an OpenClaw gateway to a new machine without redoing onboarding.
|
||||
|
||||
The migration is simple conceptually:
|
||||
## What Gets Migrated
|
||||
|
||||
- Copy the **state directory** (`$OPENCLAW_STATE_DIR`, default: `~/.openclaw/`) — this includes config, auth, sessions, and channel state.
|
||||
- Copy your **workspace** (`~/.openclaw/workspace/` by default) — this includes your agent files (memory, prompts, etc.).
|
||||
When you copy the **state directory** (`~/.openclaw/` by default) and your **workspace**, you preserve:
|
||||
|
||||
But there are common footguns around **profiles**, **permissions**, and **partial copies**.
|
||||
- **Config** -- `openclaw.json` and all gateway settings
|
||||
- **Auth** -- API keys, OAuth tokens, credential profiles
|
||||
- **Sessions** -- conversation history and agent state
|
||||
- **Channel state** -- WhatsApp login, Telegram session, etc.
|
||||
- **Workspace files** -- `MEMORY.md`, `USER.md`, skills, and prompts
|
||||
|
||||
## Before you start (what you are migrating)
|
||||
<Tip>
|
||||
Run `openclaw status` on the old machine to confirm your state directory path.
|
||||
Custom profiles use `~/.openclaw-<profile>/` or a path set via `OPENCLAW_STATE_DIR`.
|
||||
</Tip>
|
||||
|
||||
### 1) Identify your state directory
|
||||
## Migration Steps
|
||||
|
||||
Most installs use the default:
|
||||
<Steps>
|
||||
<Step title="Stop the gateway and back up">
|
||||
On the **old** machine, stop the gateway so files are not changing mid-copy, then archive:
|
||||
|
||||
- **State dir:** `~/.openclaw/`
|
||||
```bash
|
||||
openclaw gateway stop
|
||||
cd ~
|
||||
tar -czf openclaw-state.tgz .openclaw
|
||||
```
|
||||
|
||||
But it may be different if you use:
|
||||
If you use multiple profiles (e.g. `~/.openclaw-work`), archive each separately.
|
||||
|
||||
- `--profile <name>` (often becomes `~/.openclaw-<profile>/`)
|
||||
- `OPENCLAW_STATE_DIR=/some/path`
|
||||
</Step>
|
||||
|
||||
If you’re not sure, run on the **old** machine:
|
||||
<Step title="Install OpenClaw on the new machine">
|
||||
[Install](/install) the CLI (and Node if needed) on the new machine.
|
||||
It is fine if onboarding creates a fresh `~/.openclaw/` -- you will overwrite it next.
|
||||
</Step>
|
||||
|
||||
```bash
|
||||
openclaw status
|
||||
```
|
||||
<Step title="Copy state directory and workspace">
|
||||
Transfer the archive via `scp`, `rsync -a`, or an external drive, then extract:
|
||||
|
||||
Look for mentions of `OPENCLAW_STATE_DIR` / profile in the output. If you run multiple gateways, repeat for each profile.
|
||||
```bash
|
||||
cd ~
|
||||
tar -xzf openclaw-state.tgz
|
||||
```
|
||||
|
||||
### 2) Identify your workspace
|
||||
Ensure hidden directories were included and file ownership matches the user that will run the gateway.
|
||||
|
||||
Common defaults:
|
||||
</Step>
|
||||
|
||||
- `~/.openclaw/workspace/` (recommended workspace)
|
||||
- a custom folder you created
|
||||
<Step title="Run doctor and verify">
|
||||
On the new machine, run [Doctor](/gateway/doctor) to apply config migrations and repair services:
|
||||
|
||||
Your workspace is where files like `MEMORY.md`, `USER.md`, and `memory/*.md` live.
|
||||
```bash
|
||||
openclaw doctor
|
||||
openclaw gateway restart
|
||||
openclaw status
|
||||
```
|
||||
|
||||
### 3) Understand what you will preserve
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
If you copy **both** the state dir and workspace, you keep:
|
||||
## Common Pitfalls
|
||||
|
||||
- Gateway configuration (`openclaw.json`)
|
||||
- Auth profiles / API keys / OAuth tokens
|
||||
- Session history + agent state
|
||||
- Channel state (e.g. WhatsApp login/session)
|
||||
- Your workspace files (memory, skills notes, etc.)
|
||||
<AccordionGroup>
|
||||
<Accordion title="Profile or state-dir mismatch">
|
||||
If the old gateway used `--profile` or `OPENCLAW_STATE_DIR` and the new one does not,
|
||||
channels will appear logged out and sessions will be empty.
|
||||
Launch the gateway with the **same** profile or state-dir you migrated, then rerun `openclaw doctor`.
|
||||
</Accordion>
|
||||
|
||||
If you copy **only** the workspace (e.g., via Git), you do **not** preserve:
|
||||
<Accordion title="Copying only openclaw.json">
|
||||
The config file alone is not enough. Credentials live under `credentials/`, and agent
|
||||
state lives under `agents/`. Always migrate the **entire** state directory.
|
||||
</Accordion>
|
||||
|
||||
- sessions
|
||||
- credentials
|
||||
- channel logins
|
||||
<Accordion title="Permissions and ownership">
|
||||
If you copied as root or switched users, the gateway may fail to read credentials.
|
||||
Ensure the state directory and workspace are owned by the user running the gateway.
|
||||
</Accordion>
|
||||
|
||||
Those live under `$OPENCLAW_STATE_DIR`.
|
||||
<Accordion title="Remote mode">
|
||||
If your UI points at a **remote** gateway, the remote host owns sessions and workspace.
|
||||
Migrate the gateway host itself, not your local laptop. See [FAQ](/help/faq#where-does-openclaw-store-its-data).
|
||||
</Accordion>
|
||||
|
||||
## Migration steps (recommended)
|
||||
<Accordion title="Secrets in backups">
|
||||
The state directory contains API keys, OAuth tokens, and channel credentials.
|
||||
Store backups encrypted, avoid insecure transfer channels, and rotate keys if you suspect exposure.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Step 0 - Make a backup (old machine)
|
||||
|
||||
On the **old** machine, stop the gateway first so files aren’t changing mid-copy:
|
||||
|
||||
```bash
|
||||
openclaw gateway stop
|
||||
```
|
||||
|
||||
(Optional but recommended) archive the state dir and workspace:
|
||||
|
||||
```bash
|
||||
# Adjust paths if you use a profile or custom locations
|
||||
cd ~
|
||||
tar -czf openclaw-state.tgz .openclaw
|
||||
|
||||
tar -czf openclaw-workspace.tgz .openclaw/workspace
|
||||
```
|
||||
|
||||
If you have multiple profiles/state dirs (e.g. `~/.openclaw-main`, `~/.openclaw-work`), archive each.
|
||||
|
||||
### Step 1 - Install OpenClaw on the new machine
|
||||
|
||||
On the **new** machine, install the CLI (and Node if needed):
|
||||
|
||||
- See: [Install](/install)
|
||||
|
||||
At this stage, it’s OK if onboarding creates a fresh `~/.openclaw/` — you will overwrite it in the next step.
|
||||
|
||||
### Step 2 - Copy the state dir + workspace to the new machine
|
||||
|
||||
Copy **both**:
|
||||
|
||||
- `$OPENCLAW_STATE_DIR` (default `~/.openclaw/`)
|
||||
- your workspace (default `~/.openclaw/workspace/`)
|
||||
|
||||
Common approaches:
|
||||
|
||||
- `scp` the tarballs and extract
|
||||
- `rsync -a` over SSH
|
||||
- external drive
|
||||
|
||||
After copying, ensure:
|
||||
|
||||
- Hidden directories were included (e.g. `.openclaw/`)
|
||||
- File ownership is correct for the user running the gateway
|
||||
|
||||
### Step 3 - Run Doctor (migrations + service repair)
|
||||
|
||||
On the **new** machine:
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
Doctor is the “safe boring” command. It repairs services, applies config migrations, and warns about mismatches.
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
openclaw status
|
||||
```
|
||||
|
||||
## Common footguns (and how to avoid them)
|
||||
|
||||
### Footgun: profile / state-dir mismatch
|
||||
|
||||
If you ran the old gateway with a profile (or `OPENCLAW_STATE_DIR`), and the new gateway uses a different one, you’ll see symptoms like:
|
||||
|
||||
- config changes not taking effect
|
||||
- channels missing / logged out
|
||||
- empty session history
|
||||
|
||||
Fix: run the gateway/service using the **same** profile/state dir you migrated, then rerun:
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
### Footgun: copying only `openclaw.json`
|
||||
|
||||
`openclaw.json` is not enough. Many providers store state under:
|
||||
|
||||
- `$OPENCLAW_STATE_DIR/credentials/`
|
||||
- `$OPENCLAW_STATE_DIR/agents/<agentId>/...`
|
||||
|
||||
Always migrate the entire `$OPENCLAW_STATE_DIR` folder.
|
||||
|
||||
### Footgun: permissions / ownership
|
||||
|
||||
If you copied as root or changed users, the gateway may fail to read credentials/sessions.
|
||||
|
||||
Fix: ensure the state dir + workspace are owned by the user running the gateway.
|
||||
|
||||
### Footgun: migrating between remote/local modes
|
||||
|
||||
- If your UI (WebUI/TUI) points at a **remote** gateway, the remote host owns the session store + workspace.
|
||||
- Migrating your laptop won’t move the remote gateway’s state.
|
||||
|
||||
If you’re in remote mode, migrate the **gateway host**.
|
||||
|
||||
### Footgun: secrets in backups
|
||||
|
||||
`$OPENCLAW_STATE_DIR` contains secrets (API keys, OAuth tokens, WhatsApp creds). Treat backups like production secrets:
|
||||
|
||||
- store encrypted
|
||||
- avoid sharing over insecure channels
|
||||
- rotate keys if you suspect exposure
|
||||
|
||||
## Verification checklist
|
||||
## Verification Checklist
|
||||
|
||||
On the new machine, confirm:
|
||||
|
||||
- `openclaw status` shows the gateway running
|
||||
- Your channels are still connected (e.g. WhatsApp doesn’t require re-pair)
|
||||
- The dashboard opens and shows existing sessions
|
||||
- Your workspace files (memory, configs) are present
|
||||
|
||||
## Related
|
||||
|
||||
- [Doctor](/gateway/doctor)
|
||||
- [Gateway troubleshooting](/gateway/troubleshooting)
|
||||
- [Where does OpenClaw store its data?](/help/faq#where-does-openclaw-store-its-data)
|
||||
- [ ] `openclaw status` shows the gateway running
|
||||
- [ ] Channels are still connected (no re-pairing needed)
|
||||
- [ ] The dashboard opens and shows existing sessions
|
||||
- [ ] Workspace files (memory, configs) are present
|
||||
|
||||
@ -9,90 +9,81 @@ title: "Nix"
|
||||
|
||||
# Nix Installation
|
||||
|
||||
The recommended way to run OpenClaw with Nix is via **[nix-openclaw](https://github.com/openclaw/nix-openclaw)** — a batteries-included Home Manager module.
|
||||
Install OpenClaw declaratively with **[nix-openclaw](https://github.com/openclaw/nix-openclaw)** -- a batteries-included Home Manager module.
|
||||
|
||||
## Quick Start
|
||||
<Info>
|
||||
The [nix-openclaw](https://github.com/openclaw/nix-openclaw) repo is the source of truth for Nix installation. This page is a quick overview.
|
||||
</Info>
|
||||
|
||||
Paste this to your AI agent (Claude, Cursor, etc.):
|
||||
## What You Get
|
||||
|
||||
```text
|
||||
I want to set up nix-openclaw on my Mac.
|
||||
Repository: github:openclaw/nix-openclaw
|
||||
|
||||
What I need you to do:
|
||||
1. Check if Determinate Nix is installed (if not, install it)
|
||||
2. Create a local flake at ~/code/openclaw-local using templates/agent-first/flake.nix
|
||||
3. Help me create a Telegram bot (@BotFather) and get my chat ID (@userinfobot)
|
||||
4. Set up secrets (bot token, model provider API key) - plain files at ~/.secrets/ is fine
|
||||
5. Fill in the template placeholders and run home-manager switch
|
||||
6. Verify: launchd running, bot responds to messages
|
||||
|
||||
Reference the nix-openclaw README for module options.
|
||||
```
|
||||
|
||||
> **📦 Full guide: [github.com/openclaw/nix-openclaw](https://github.com/openclaw/nix-openclaw)**
|
||||
>
|
||||
> The nix-openclaw repo is the source of truth for Nix installation. This page is just a quick overview.
|
||||
|
||||
## What you get
|
||||
|
||||
- Gateway + macOS app + tools (whisper, spotify, cameras) — all pinned
|
||||
- Gateway + macOS app + tools (whisper, spotify, cameras) -- all pinned
|
||||
- Launchd service that survives reboots
|
||||
- Plugin system with declarative config
|
||||
- Instant rollback: `home-manager switch --rollback`
|
||||
|
||||
---
|
||||
## Quick Start
|
||||
|
||||
<Steps>
|
||||
<Step title="Install Determinate Nix">
|
||||
If Nix is not already installed, follow the [Determinate Nix installer](https://github.com/DeterminateSystems/nix-installer) instructions.
|
||||
</Step>
|
||||
<Step title="Create a local flake">
|
||||
Use the agent-first template from the nix-openclaw repo:
|
||||
```bash
|
||||
mkdir -p ~/code/openclaw-local
|
||||
# Copy templates/agent-first/flake.nix from the nix-openclaw repo
|
||||
```
|
||||
</Step>
|
||||
<Step title="Configure secrets">
|
||||
Set up your messaging bot token and model provider API key. Plain files at `~/.secrets/` work fine.
|
||||
</Step>
|
||||
<Step title="Fill in template placeholders and switch">
|
||||
```bash
|
||||
home-manager switch
|
||||
```
|
||||
</Step>
|
||||
<Step title="Verify">
|
||||
Confirm the launchd service is running and your bot responds to messages.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
See the [nix-openclaw README](https://github.com/openclaw/nix-openclaw) for full module options and examples.
|
||||
|
||||
## Nix Mode Runtime Behavior
|
||||
|
||||
When `OPENCLAW_NIX_MODE=1` is set (automatic with nix-openclaw):
|
||||
When `OPENCLAW_NIX_MODE=1` is set (automatic with nix-openclaw), OpenClaw enters a deterministic mode that disables auto-install flows.
|
||||
|
||||
OpenClaw supports a **Nix mode** that makes configuration deterministic and disables auto-install flows.
|
||||
Enable it by exporting:
|
||||
You can also set it manually:
|
||||
|
||||
```bash
|
||||
OPENCLAW_NIX_MODE=1
|
||||
export OPENCLAW_NIX_MODE=1
|
||||
```
|
||||
|
||||
On macOS, the GUI app does not automatically inherit shell env vars. You can
|
||||
also enable Nix mode via defaults:
|
||||
On macOS, the GUI app does not automatically inherit shell environment variables. Enable Nix mode via defaults instead:
|
||||
|
||||
```bash
|
||||
defaults write ai.openclaw.mac openclaw.nixMode -bool true
|
||||
```
|
||||
|
||||
### Config + state paths
|
||||
|
||||
OpenClaw reads JSON5 config from `OPENCLAW_CONFIG_PATH` and stores mutable data in `OPENCLAW_STATE_DIR`.
|
||||
When needed, you can also set `OPENCLAW_HOME` to control the base home directory used for internal path resolution.
|
||||
|
||||
- `OPENCLAW_HOME` (default precedence: `HOME` / `USERPROFILE` / `os.homedir()`)
|
||||
- `OPENCLAW_STATE_DIR` (default: `~/.openclaw`)
|
||||
- `OPENCLAW_CONFIG_PATH` (default: `$OPENCLAW_STATE_DIR/openclaw.json`)
|
||||
|
||||
When running under Nix, set these explicitly to Nix-managed locations so runtime state and config
|
||||
stay out of the immutable store.
|
||||
|
||||
### Runtime behavior in Nix mode
|
||||
### What changes in Nix mode
|
||||
|
||||
- Auto-install and self-mutation flows are disabled
|
||||
- Missing dependencies surface Nix-specific remediation messages
|
||||
- UI surfaces a read-only Nix mode banner when present
|
||||
- UI surfaces a read-only Nix mode banner
|
||||
|
||||
## Packaging note (macOS)
|
||||
### Config and state paths
|
||||
|
||||
The macOS packaging flow expects a stable Info.plist template at:
|
||||
OpenClaw reads JSON5 config from `OPENCLAW_CONFIG_PATH` and stores mutable data in `OPENCLAW_STATE_DIR`. When running under Nix, set these explicitly to Nix-managed locations so runtime state and config stay out of the immutable store.
|
||||
|
||||
```
|
||||
apps/macos/Sources/OpenClaw/Resources/Info.plist
|
||||
```
|
||||
|
||||
[`scripts/package-mac-app.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/package-mac-app.sh) copies this template into the app bundle and patches dynamic fields
|
||||
(bundle ID, version/build, Git SHA, Sparkle keys). This keeps the plist deterministic for SwiftPM
|
||||
packaging and Nix builds (which do not rely on a full Xcode toolchain).
|
||||
| Variable | Default |
|
||||
| ---------------------- | --------------------------------------- |
|
||||
| `OPENCLAW_HOME` | `HOME` / `USERPROFILE` / `os.homedir()` |
|
||||
| `OPENCLAW_STATE_DIR` | `~/.openclaw` |
|
||||
| `OPENCLAW_CONFIG_PATH` | `$OPENCLAW_STATE_DIR/openclaw.json` |
|
||||
|
||||
## Related
|
||||
|
||||
- [nix-openclaw](https://github.com/openclaw/nix-openclaw) — full setup guide
|
||||
- [Wizard](/start/wizard) — non-Nix CLI setup
|
||||
- [Docker](/install/docker) — containerized setup
|
||||
- [nix-openclaw](https://github.com/openclaw/nix-openclaw) -- full setup guide
|
||||
- [Wizard](/start/wizard) -- non-Nix CLI setup
|
||||
- [Docker](/install/docker) -- containerized setup
|
||||
|
||||
@ -9,7 +9,7 @@ read_when:
|
||||
|
||||
# Node.js
|
||||
|
||||
OpenClaw requires **Node 22.16 or newer**. **Node 24 is the default and recommended runtime** for installs, CI, and release workflows. Node 22 remains supported via the active LTS line. The [installer script](/install#install-methods) will detect and install Node automatically — this page is for when you want to set up Node yourself and make sure everything is wired up correctly (versions, PATH, global installs).
|
||||
OpenClaw requires **Node 22.16 or newer**. **Node 24 is the default and recommended runtime** for installs, CI, and release workflows. Node 22 remains supported via the active LTS line. The [installer script](/install#alternative-install-methods) will detect and install Node automatically — this page is for when you want to set up Node yourself and make sure everything is wired up correctly (versions, PATH, global installs).
|
||||
|
||||
## Check your version
|
||||
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
---
|
||||
title: Deploy on Northflank
|
||||
summary: "Deploy OpenClaw on Northflank with one-click template"
|
||||
read_when:
|
||||
- Deploying OpenClaw to Northflank
|
||||
- You want a one-click cloud deploy with browser-based setup
|
||||
title: "Northflank"
|
||||
---
|
||||
|
||||
Deploy OpenClaw on Northflank with a one-click template and finish setup in your browser.
|
||||
@ -34,20 +38,17 @@ and you configure everything via the `/setup` web wizard.
|
||||
|
||||
If Telegram DMs are set to pairing, web setup can approve the pairing code.
|
||||
|
||||
## Getting chat tokens
|
||||
## Connect a channel
|
||||
|
||||
### Telegram bot token
|
||||
Paste your Telegram or Discord token into the `/setup` wizard. For setup
|
||||
instructions, see the channel docs:
|
||||
|
||||
1. Message `@BotFather` in Telegram
|
||||
2. Run `/newbot`
|
||||
3. Copy the token (looks like `123456789:AA...`)
|
||||
4. Paste it into `/setup`
|
||||
- [Telegram](/channels/telegram) (fastest — just a bot token)
|
||||
- [Discord](/channels/discord)
|
||||
- [All channels](/channels)
|
||||
|
||||
### Discord bot token
|
||||
## Next steps
|
||||
|
||||
1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications)
|
||||
2. **New Application** → choose a name
|
||||
3. **Bot** → **Add Bot**
|
||||
4. **Enable MESSAGE CONTENT INTENT** under Bot → Privileged Gateway Intents (required or the bot will crash on startup)
|
||||
5. Copy the **Bot Token** and paste into `/setup`
|
||||
6. Invite the bot to your server (OAuth2 URL Generator; scopes: `bot`, `applications.commands`)
|
||||
- Set up messaging channels: [Channels](/channels)
|
||||
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
|
||||
- Keep OpenClaw up to date: [Updating](/install/updating)
|
||||
|
||||
156
docs/install/oracle.md
Normal file
156
docs/install/oracle.md
Normal file
@ -0,0 +1,156 @@
|
||||
---
|
||||
summary: "Host OpenClaw on Oracle Cloud's Always Free ARM tier"
|
||||
read_when:
|
||||
- Setting up OpenClaw on Oracle Cloud
|
||||
- Looking for free VPS hosting for OpenClaw
|
||||
- Want 24/7 OpenClaw on a small server
|
||||
title: "Oracle Cloud"
|
||||
---
|
||||
|
||||
# Oracle Cloud
|
||||
|
||||
Run a persistent OpenClaw Gateway on Oracle Cloud's **Always Free** ARM tier (up to 4 OCPU, 24 GB RAM, 200 GB storage) at no cost.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Oracle Cloud account ([signup](https://www.oracle.com/cloud/free/)) -- see [community signup guide](https://gist.github.com/rssnyder/51e3cfedd730e7dd5f4a816143b25dbd) if you hit issues
|
||||
- Tailscale account (free at [tailscale.com](https://tailscale.com))
|
||||
- An SSH key pair
|
||||
- About 30 minutes
|
||||
|
||||
## Setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an OCI instance">
|
||||
1. Log into [Oracle Cloud Console](https://cloud.oracle.com/).
|
||||
2. Navigate to **Compute > Instances > Create Instance**.
|
||||
3. Configure:
|
||||
- **Name:** `openclaw`
|
||||
- **Image:** Ubuntu 24.04 (aarch64)
|
||||
- **Shape:** `VM.Standard.A1.Flex` (Ampere ARM)
|
||||
- **OCPUs:** 2 (or up to 4)
|
||||
- **Memory:** 12 GB (or up to 24 GB)
|
||||
- **Boot volume:** 50 GB (up to 200 GB free)
|
||||
- **SSH key:** Add your public key
|
||||
4. Click **Create** and note the public IP address.
|
||||
|
||||
<Tip>
|
||||
If instance creation fails with "Out of capacity", try a different availability domain or retry later. Free tier capacity is limited.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Connect and update the system">
|
||||
```bash
|
||||
ssh ubuntu@YOUR_PUBLIC_IP
|
||||
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
sudo apt install -y build-essential
|
||||
```
|
||||
|
||||
`build-essential` is required for ARM compilation of some dependencies.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Configure user and hostname">
|
||||
```bash
|
||||
sudo hostnamectl set-hostname openclaw
|
||||
sudo passwd ubuntu
|
||||
sudo loginctl enable-linger ubuntu
|
||||
```
|
||||
|
||||
Enabling linger keeps user services running after logout.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Install Tailscale">
|
||||
```bash
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
sudo tailscale up --ssh --hostname=openclaw
|
||||
```
|
||||
|
||||
From now on, connect via Tailscale: `ssh ubuntu@openclaw`.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Install OpenClaw">
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
When prompted "How do you want to hatch your bot?", select **Do this later**.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Configure the gateway">
|
||||
Use token auth with Tailscale Serve for secure remote access.
|
||||
|
||||
```bash
|
||||
openclaw config set gateway.bind loopback
|
||||
openclaw config set gateway.auth.mode token
|
||||
openclaw doctor --generate-gateway-token
|
||||
openclaw config set gateway.tailscale.mode serve
|
||||
openclaw config set gateway.trustedProxies '["127.0.0.1"]'
|
||||
|
||||
systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Lock down VCN security">
|
||||
Block all traffic except Tailscale at the network edge:
|
||||
|
||||
1. Go to **Networking > Virtual Cloud Networks** in the OCI Console.
|
||||
2. Click your VCN, then **Security Lists > Default Security List**.
|
||||
3. **Remove** all ingress rules except `0.0.0.0/0 UDP 41641` (Tailscale).
|
||||
4. Keep default egress rules (allow all outbound).
|
||||
|
||||
This blocks SSH on port 22, HTTP, HTTPS, and everything else at the network edge. You can only connect via Tailscale from this point on.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Verify">
|
||||
```bash
|
||||
openclaw --version
|
||||
systemctl --user status openclaw-gateway
|
||||
tailscale serve status
|
||||
curl http://localhost:18789
|
||||
```
|
||||
|
||||
Access the Control UI from any device on your tailnet:
|
||||
|
||||
```
|
||||
https://openclaw.<tailnet-name>.ts.net/
|
||||
```
|
||||
|
||||
Replace `<tailnet-name>` with your tailnet name (visible in `tailscale status`).
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Fallback: SSH tunnel
|
||||
|
||||
If Tailscale Serve is not working, use an SSH tunnel from your local machine:
|
||||
|
||||
```bash
|
||||
ssh -L 18789:127.0.0.1:18789 ubuntu@openclaw
|
||||
```
|
||||
|
||||
Then open `http://localhost:18789`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Instance creation fails ("Out of capacity")** -- Free tier ARM instances are popular. Try a different availability domain or retry during off-peak hours.
|
||||
|
||||
**Tailscale will not connect** -- Run `sudo tailscale up --ssh --hostname=openclaw --reset` to re-authenticate.
|
||||
|
||||
**Gateway will not start** -- Run `openclaw doctor --non-interactive` and check logs with `journalctl --user -u openclaw-gateway -n 50`.
|
||||
|
||||
**ARM binary issues** -- Most npm packages work on ARM64. For native binaries, look for `linux-arm64` or `aarch64` releases. Verify architecture with `uname -m`.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Channels](/channels) -- connect Telegram, WhatsApp, Discord, and more
|
||||
- [Gateway configuration](/gateway/configuration) -- all config options
|
||||
- [Updating](/install/updating) -- keep OpenClaw up to date
|
||||
@ -7,53 +7,64 @@ title: "Podman"
|
||||
|
||||
# Podman
|
||||
|
||||
Run the OpenClaw gateway in a **rootless** Podman container. Uses the same image as Docker (build from the repo [Dockerfile](https://github.com/openclaw/openclaw/blob/main/Dockerfile)).
|
||||
Run the OpenClaw Gateway in a **rootless** Podman container. Uses the same image as Docker (built from the repo [Dockerfile](https://github.com/openclaw/openclaw/blob/main/Dockerfile)).
|
||||
|
||||
## Requirements
|
||||
## Prerequisites
|
||||
|
||||
- Podman (rootless)
|
||||
- Sudo for one-time setup (create user, build image)
|
||||
- **Podman** (rootless mode)
|
||||
- **sudo** access for one-time setup (creating the dedicated user and building the image)
|
||||
|
||||
## Quick start
|
||||
|
||||
**1. One-time setup** (from repo root; creates user, builds image, installs launch script):
|
||||
<Steps>
|
||||
<Step title="One-time setup">
|
||||
From the repo root, run the setup script. It creates a dedicated `openclaw` user, builds the container image, and installs the launch script:
|
||||
|
||||
```bash
|
||||
./setup-podman.sh
|
||||
```
|
||||
```bash
|
||||
./scripts/podman/setup.sh
|
||||
```
|
||||
|
||||
This also creates a minimal `~openclaw/.openclaw/openclaw.json` (sets `gateway.mode="local"`) so the gateway can start without running the wizard.
|
||||
This also creates a minimal config at `~openclaw/.openclaw/openclaw.json` (sets `gateway.mode` to `"local"`) so the Gateway can start without running the wizard.
|
||||
|
||||
By default the container is **not** installed as a systemd service, you start it manually (see below). For a production-style setup with auto-start and restarts, install it as a systemd Quadlet user service instead:
|
||||
By default the container is **not** installed as a systemd service -- you start it manually in the next step. For a production-style setup with auto-start and restarts, pass `--quadlet` instead:
|
||||
|
||||
```bash
|
||||
./setup-podman.sh --quadlet
|
||||
```
|
||||
```bash
|
||||
./scripts/podman/setup.sh --quadlet
|
||||
```
|
||||
|
||||
(Or set `OPENCLAW_PODMAN_QUADLET=1`; use `--container` to install only the container and launch script.)
|
||||
(Or set `OPENCLAW_PODMAN_QUADLET=1`. Use `--container` to install only the container and launch script.)
|
||||
|
||||
Optional build-time env vars (set before running `setup-podman.sh`):
|
||||
**Optional build-time env vars** (set before running `scripts/podman/setup.sh`):
|
||||
|
||||
- `OPENCLAW_DOCKER_APT_PACKAGES` — install extra apt packages during image build
|
||||
- `OPENCLAW_EXTENSIONS` — pre-install extension dependencies (space-separated extension names, e.g. `diagnostics-otel matrix`)
|
||||
- `OPENCLAW_DOCKER_APT_PACKAGES` -- install extra apt packages during image build.
|
||||
- `OPENCLAW_EXTENSIONS` -- pre-install extension dependencies (space-separated names, e.g. `diagnostics-otel matrix`).
|
||||
|
||||
**2. Start gateway** (manual, for quick smoke testing):
|
||||
</Step>
|
||||
|
||||
```bash
|
||||
./scripts/run-openclaw-podman.sh launch
|
||||
```
|
||||
<Step title="Start the Gateway">
|
||||
For a quick manual launch:
|
||||
|
||||
**3. Onboarding wizard** (e.g. to add channels or providers):
|
||||
```bash
|
||||
./scripts/run-openclaw-podman.sh launch
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/run-openclaw-podman.sh launch setup
|
||||
```
|
||||
</Step>
|
||||
|
||||
Then open `http://127.0.0.1:18789/` and use the token from `~openclaw/.openclaw/.env` (or the value printed by setup).
|
||||
<Step title="Run the onboarding wizard">
|
||||
To add channels or providers interactively:
|
||||
|
||||
```bash
|
||||
./scripts/run-openclaw-podman.sh launch setup
|
||||
```
|
||||
|
||||
Then open `http://127.0.0.1:18789/` and use the token from `~openclaw/.openclaw/.env` (or the value printed by setup).
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Systemd (Quadlet, optional)
|
||||
|
||||
If you ran `./setup-podman.sh --quadlet` (or `OPENCLAW_PODMAN_QUADLET=1`), a [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) unit is installed so the gateway runs as a systemd user service for the openclaw user. The service is enabled and started at the end of setup.
|
||||
If you ran `./scripts/podman/setup.sh --quadlet` (or `OPENCLAW_PODMAN_QUADLET=1`), a [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) unit is installed so the gateway runs as a systemd user service for the openclaw user. The service is enabled and started at the end of setup.
|
||||
|
||||
- **Start:** `sudo systemctl --machine openclaw@ --user start openclaw.service`
|
||||
- **Stop:** `sudo systemctl --machine openclaw@ --user stop openclaw.service`
|
||||
@ -62,11 +73,11 @@ If you ran `./setup-podman.sh --quadlet` (or `OPENCLAW_PODMAN_QUADLET=1`), a [Po
|
||||
|
||||
The quadlet file lives at `~openclaw/.config/containers/systemd/openclaw.container`. To change ports or env, edit that file (or the `.env` it sources), then `sudo systemctl --machine openclaw@ --user daemon-reload` and restart the service. On boot, the service starts automatically if lingering is enabled for openclaw (setup does this when loginctl is available).
|
||||
|
||||
To add quadlet **after** an initial setup that did not use it, re-run: `./setup-podman.sh --quadlet`.
|
||||
To add quadlet **after** an initial setup that did not use it, re-run: `./scripts/podman/setup.sh --quadlet`.
|
||||
|
||||
## The openclaw user (non-login)
|
||||
|
||||
`setup-podman.sh` creates a dedicated system user `openclaw`:
|
||||
`scripts/podman/setup.sh` creates a dedicated system user `openclaw`:
|
||||
|
||||
- **Shell:** `nologin` — no interactive login; reduces attack surface.
|
||||
- **Home:** e.g. `/home/openclaw` — holds `~/.openclaw` (config, workspace) and the launch script `run-openclaw-podman.sh`.
|
||||
@ -87,7 +98,7 @@ To add quadlet **after** an initial setup that did not use it, re-run: `./setup-
|
||||
|
||||
## Environment and config
|
||||
|
||||
- **Token:** Stored in `~openclaw/.openclaw/.env` as `OPENCLAW_GATEWAY_TOKEN`. `setup-podman.sh` and `run-openclaw-podman.sh` generate it if missing (uses `openssl`, `python3`, or `od`).
|
||||
- **Token:** Stored in `~openclaw/.openclaw/.env` as `OPENCLAW_GATEWAY_TOKEN`. `scripts/podman/setup.sh` and `run-openclaw-podman.sh` generate it if missing (uses `openssl`, `python3`, or `od`).
|
||||
- **Optional:** In that `.env` you can set provider keys (e.g. `GROQ_API_KEY`, `OLLAMA_API_KEY`) and other OpenClaw env vars.
|
||||
- **Host ports:** By default the script maps `18789` (gateway) and `18790` (bridge). Override the **host** port mapping with `OPENCLAW_PODMAN_GATEWAY_HOST_PORT` and `OPENCLAW_PODMAN_BRIDGE_HOST_PORT` when launching.
|
||||
- **Gateway bind:** By default, `run-openclaw-podman.sh` starts the gateway with `--bind loopback` for safe local access. To expose on LAN, set `OPENCLAW_GATEWAY_BIND=lan` and configure `gateway.controlUi.allowedOrigins` (or explicitly enable host-header fallback) in `openclaw.json`.
|
||||
@ -99,7 +110,7 @@ To add quadlet **after** an initial setup that did not use it, re-run: `./setup-
|
||||
- **Ephemeral sandbox tmpfs:** if you enable `agents.defaults.sandbox`, the tool sandbox containers mount `tmpfs` at `/tmp`, `/var/tmp`, and `/run`. Those paths are memory-backed and disappear with the sandbox container; the top-level Podman container setup does not add its own tmpfs mounts.
|
||||
- **Disk growth hotspots:** the main paths to watch are `media/`, `agents/<agentId>/sessions/sessions.json`, transcript JSONL files, `cron/runs/*.jsonl`, and rolling file logs under `/tmp/openclaw/` (or your configured `logging.file`).
|
||||
|
||||
`setup-podman.sh` now stages the image tar in a private temp directory and prints the chosen base dir during setup. For non-root runs it accepts `TMPDIR` only when that base is safe to use; otherwise it falls back to `/var/tmp`, then `/tmp`. The saved tar stays owner-only and is streamed into the target user’s `podman load`, so private caller temp dirs do not block setup.
|
||||
`scripts/podman/setup.sh` now stages the image tar in a private temp directory and prints the chosen base dir during setup. For non-root runs it accepts `TMPDIR` only when that base is safe to use; otherwise it falls back to `/var/tmp`, then `/tmp`. The saved tar stays owner-only and is streamed into the target user’s `podman load`, so private caller temp dirs do not block setup.
|
||||
|
||||
## Useful commands
|
||||
|
||||
@ -111,12 +122,12 @@ To add quadlet **after** an initial setup that did not use it, re-run: `./setup-
|
||||
## Troubleshooting
|
||||
|
||||
- **Permission denied (EACCES) on config or auth-profiles:** The container defaults to `--userns=keep-id` and runs as the same uid/gid as the host user running the script. Ensure your host `OPENCLAW_CONFIG_DIR` and `OPENCLAW_WORKSPACE_DIR` are owned by that user.
|
||||
- **Gateway start blocked (missing `gateway.mode=local`):** Ensure `~openclaw/.openclaw/openclaw.json` exists and sets `gateway.mode="local"`. `setup-podman.sh` creates this file if missing.
|
||||
- **Gateway start blocked (missing `gateway.mode=local`):** Ensure `~openclaw/.openclaw/openclaw.json` exists and sets `gateway.mode="local"`. `scripts/podman/setup.sh` creates this file if missing.
|
||||
- **Rootless Podman fails for user openclaw:** Check `/etc/subuid` and `/etc/subgid` contain a line for `openclaw` (e.g. `openclaw:100000:65536`). Add it if missing and restart.
|
||||
- **Container name in use:** The launch script uses `podman run --replace`, so the existing container is replaced when you start again. To clean up manually: `podman rm -f openclaw`.
|
||||
- **Script not found when running as openclaw:** Ensure `setup-podman.sh` was run so that `run-openclaw-podman.sh` is copied to openclaw’s home (e.g. `/home/openclaw/run-openclaw-podman.sh`).
|
||||
- **Script not found when running as openclaw:** Ensure `scripts/podman/setup.sh` was run so that `run-openclaw-podman.sh` is copied to openclaw’s home (e.g. `/home/openclaw/run-openclaw-podman.sh`).
|
||||
- **Quadlet service not found or fails to start:** Run `sudo systemctl --machine openclaw@ --user daemon-reload` after editing the `.container` file. Quadlet requires cgroups v2: `podman info --format '{{.Host.CgroupsVersion}}'` should show `2`.
|
||||
|
||||
## Optional: run as your own user
|
||||
|
||||
To run the gateway as your normal user (no dedicated openclaw user): build the image, create `~/.openclaw/.env` with `OPENCLAW_GATEWAY_TOKEN`, and run the container with `--userns=keep-id` and mounts to your `~/.openclaw`. The launch script is designed for the openclaw-user flow; for a single-user setup you can instead run the `podman run` command from the script manually, pointing config and workspace to your home. Recommended for most users: use `setup-podman.sh` and run as the openclaw user so config and process are isolated.
|
||||
To run the gateway as your normal user (no dedicated openclaw user): build the image, create `~/.openclaw/.env` with `OPENCLAW_GATEWAY_TOKEN`, and run the container with `--userns=keep-id` and mounts to your `~/.openclaw`. The launch script is designed for the openclaw-user flow; for a single-user setup you can instead run the `podman run` command from the script manually, pointing config and workspace to your home. Recommended for most users: use `scripts/podman/setup.sh` and run as the openclaw user so config and process are isolated.
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
---
|
||||
title: Deploy on Railway
|
||||
summary: "Deploy OpenClaw on Railway with one-click template"
|
||||
read_when:
|
||||
- Deploying OpenClaw to Railway
|
||||
- You want a one-click cloud deploy with browser-based setup
|
||||
title: "Railway"
|
||||
---
|
||||
|
||||
Deploy OpenClaw on Railway with a one-click template and finish setup in your browser.
|
||||
@ -72,23 +76,14 @@ Set these variables on the service:
|
||||
|
||||
If Telegram DMs are set to pairing, web setup can approve the pairing code.
|
||||
|
||||
## Getting chat tokens
|
||||
## Connect a channel
|
||||
|
||||
### Telegram bot token
|
||||
Paste your Telegram or Discord token into the `/setup` wizard. For setup
|
||||
instructions, see the channel docs:
|
||||
|
||||
1. Message `@BotFather` in Telegram
|
||||
2. Run `/newbot`
|
||||
3. Copy the token (looks like `123456789:AA...`)
|
||||
4. Paste it into `/setup`
|
||||
|
||||
### Discord bot token
|
||||
|
||||
1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications)
|
||||
2. **New Application** → choose a name
|
||||
3. **Bot** → **Add Bot**
|
||||
4. **Enable MESSAGE CONTENT INTENT** under Bot → Privileged Gateway Intents (required or the bot will crash on startup)
|
||||
5. Copy the **Bot Token** and paste into `/setup`
|
||||
6. Invite the bot to your server (OAuth2 URL Generator; scopes: `bot`, `applications.commands`)
|
||||
- [Telegram](/channels/telegram) (fastest — just a bot token)
|
||||
- [Discord](/channels/discord)
|
||||
- [All channels](/channels)
|
||||
|
||||
## Backups & migration
|
||||
|
||||
@ -97,3 +92,9 @@ Download a backup at:
|
||||
- `https://<your-railway-domain>/setup/export`
|
||||
|
||||
This exports your OpenClaw state + workspace so you can migrate to another host without losing config or memory.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Set up messaging channels: [Channels](/channels)
|
||||
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
|
||||
- Keep OpenClaw up to date: [Updating](/install/updating)
|
||||
|
||||
159
docs/install/raspberry-pi.md
Normal file
159
docs/install/raspberry-pi.md
Normal file
@ -0,0 +1,159 @@
|
||||
---
|
||||
summary: "Host OpenClaw on a Raspberry Pi for always-on self-hosting"
|
||||
read_when:
|
||||
- Setting up OpenClaw on a Raspberry Pi
|
||||
- Running OpenClaw on ARM devices
|
||||
- Building a cheap always-on personal AI
|
||||
title: "Raspberry Pi"
|
||||
---
|
||||
|
||||
# Raspberry Pi
|
||||
|
||||
Run a persistent, always-on OpenClaw Gateway on a Raspberry Pi. Since the Pi is just the gateway (models run in the cloud via API), even a modest Pi handles the workload well.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Raspberry Pi 4 or 5 with 2 GB+ RAM (4 GB recommended)
|
||||
- MicroSD card (16 GB+) or USB SSD (better performance)
|
||||
- Official Pi power supply
|
||||
- Network connection (Ethernet or WiFi)
|
||||
- 64-bit Raspberry Pi OS (required -- do not use 32-bit)
|
||||
- About 30 minutes
|
||||
|
||||
## Setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Flash the OS">
|
||||
Use **Raspberry Pi OS Lite (64-bit)** -- no desktop needed for a headless server.
|
||||
|
||||
1. Download [Raspberry Pi Imager](https://www.raspberrypi.com/software/).
|
||||
2. Choose OS: **Raspberry Pi OS Lite (64-bit)**.
|
||||
3. In the settings dialog, pre-configure:
|
||||
- Hostname: `gateway-host`
|
||||
- Enable SSH
|
||||
- Set username and password
|
||||
- Configure WiFi (if not using Ethernet)
|
||||
4. Flash to your SD card or USB drive, insert it, and boot the Pi.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Connect via SSH">
|
||||
```bash
|
||||
ssh user@gateway-host
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Update the system">
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
sudo apt install -y git curl build-essential
|
||||
|
||||
# Set timezone (important for cron and reminders)
|
||||
sudo timedatectl set-timezone America/Chicago
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Install Node.js 24">
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
node --version
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Add swap (important for 2 GB or less)">
|
||||
```bash
|
||||
sudo fallocate -l 2G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
||||
|
||||
# Reduce swappiness for low-RAM devices
|
||||
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Install OpenClaw">
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Run onboarding">
|
||||
```bash
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
|
||||
Follow the wizard. API keys are recommended over OAuth for headless devices. Telegram is the easiest channel to start with.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Verify">
|
||||
```bash
|
||||
openclaw status
|
||||
sudo systemctl status openclaw
|
||||
journalctl -u openclaw -f
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Access the Control UI">
|
||||
On your computer, get a dashboard URL from the Pi:
|
||||
|
||||
```bash
|
||||
ssh user@gateway-host 'openclaw dashboard --no-open'
|
||||
```
|
||||
|
||||
Then create an SSH tunnel in another terminal:
|
||||
|
||||
```bash
|
||||
ssh -N -L 18789:127.0.0.1:18789 user@gateway-host
|
||||
```
|
||||
|
||||
Open the printed URL in your local browser. For always-on remote access, see [Tailscale integration](/gateway/tailscale).
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Performance tips
|
||||
|
||||
**Use a USB SSD** -- SD cards are slow and wear out. A USB SSD dramatically improves performance. See the [Pi USB boot guide](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-mass-storage-boot).
|
||||
|
||||
**Enable module compile cache** -- Speeds up repeated CLI invocations on lower-power Pi hosts:
|
||||
|
||||
```bash
|
||||
grep -q 'NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache' ~/.bashrc || cat >> ~/.bashrc <<'EOF' # pragma: allowlist secret
|
||||
export NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache
|
||||
mkdir -p /var/tmp/openclaw-compile-cache
|
||||
export OPENCLAW_NO_RESPAWN=1
|
||||
EOF
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
**Reduce memory usage** -- For headless setups, free GPU memory and disable unused services:
|
||||
|
||||
```bash
|
||||
echo 'gpu_mem=16' | sudo tee -a /boot/config.txt
|
||||
sudo systemctl disable bluetooth
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Out of memory** -- Verify swap is active with `free -h`. Disable unused services (`sudo systemctl disable cups bluetooth avahi-daemon`). Use API-based models only.
|
||||
|
||||
**Slow performance** -- Use a USB SSD instead of an SD card. Check for CPU throttling with `vcgencmd get_throttled` (should return `0x0`).
|
||||
|
||||
**Service will not start** -- Check logs with `journalctl -u openclaw --no-pager -n 100` and run `openclaw doctor --non-interactive`.
|
||||
|
||||
**ARM binary issues** -- If a skill fails with "exec format error", check whether the binary has an ARM64 build. Verify architecture with `uname -m` (should show `aarch64`).
|
||||
|
||||
**WiFi drops** -- Disable WiFi power management: `sudo iwconfig wlan0 power off`.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Channels](/channels) -- connect Telegram, WhatsApp, Discord, and more
|
||||
- [Gateway configuration](/gateway/configuration) -- all config options
|
||||
- [Updating](/install/updating) -- keep OpenClaw up to date
|
||||
@ -1,5 +1,9 @@
|
||||
---
|
||||
title: Deploy on Render
|
||||
summary: "Deploy OpenClaw on Render with Infrastructure-as-Code"
|
||||
read_when:
|
||||
- Deploying OpenClaw to Render
|
||||
- You want a declarative cloud deploy with Render Blueprints
|
||||
title: "Render"
|
||||
---
|
||||
|
||||
Deploy OpenClaw on Render using Infrastructure as Code. The included `render.yaml` Blueprint defines your entire stack declaratively, service, disk, environment variables, so you can deploy with a single click and version your infrastructure alongside your code.
|
||||
@ -157,3 +161,9 @@ Render expects a 200 response from `/health` within 30 seconds. If builds succee
|
||||
|
||||
- Build logs for errors
|
||||
- Whether the container runs locally with `docker build && docker run`
|
||||
|
||||
## Next steps
|
||||
|
||||
- Set up messaging channels: [Channels](/channels)
|
||||
- Configure the Gateway: [Gateway configuration](/gateway/configuration)
|
||||
- Keep OpenClaw up to date: [Updating](/install/updating)
|
||||
|
||||
@ -8,44 +8,35 @@ title: "Updating"
|
||||
|
||||
# Updating
|
||||
|
||||
OpenClaw is moving fast (pre “1.0”). Treat updates like shipping infra: update → run checks → restart (or use `openclaw update`, which restarts) → verify.
|
||||
Keep OpenClaw up to date.
|
||||
|
||||
## Recommended: re-run the website installer (upgrade in place)
|
||||
## Recommended: `openclaw update`
|
||||
|
||||
The **preferred** update path is to re-run the installer from the website. It
|
||||
detects existing installs, upgrades in place, and runs `openclaw doctor` when
|
||||
needed.
|
||||
The fastest way to update. It detects your install type (npm or git), fetches the latest version, runs `openclaw doctor`, and restarts the gateway.
|
||||
|
||||
```bash
|
||||
openclaw update
|
||||
```
|
||||
|
||||
To switch channels or target a specific version:
|
||||
|
||||
```bash
|
||||
openclaw update --channel beta
|
||||
openclaw update --tag main
|
||||
openclaw update --dry-run # preview without applying
|
||||
```
|
||||
|
||||
See [Development channels](/install/development-channels) for channel semantics.
|
||||
|
||||
## Alternative: re-run the installer
|
||||
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash
|
||||
```
|
||||
|
||||
Notes:
|
||||
Add `--no-onboard` to skip onboarding. For source installs, pass `--install-method git --no-onboard`.
|
||||
|
||||
- Add `--no-onboard` if you don’t want onboarding to run again.
|
||||
- For **source installs**, use:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method git --no-onboard
|
||||
```
|
||||
|
||||
The installer will `git pull --rebase` **only** if the repo is clean.
|
||||
|
||||
- For **global installs**, the script uses `npm install -g openclaw@latest` under the hood.
|
||||
- Legacy note: `clawdbot` remains available as a compatibility shim.
|
||||
|
||||
## Before you update
|
||||
|
||||
- Know how you installed: **global** (npm/pnpm) vs **from source** (git clone).
|
||||
- Know how your Gateway is running: **foreground terminal** vs **supervised service** (launchd/systemd).
|
||||
- Snapshot your tailoring:
|
||||
- Config: `~/.openclaw/openclaw.json`
|
||||
- Credentials: `~/.openclaw/credentials/`
|
||||
- Workspace: `~/.openclaw/workspace`
|
||||
|
||||
## Update (global install)
|
||||
|
||||
Global install (pick one):
|
||||
## Alternative: manual npm or pnpm
|
||||
|
||||
```bash
|
||||
npm i -g openclaw@latest
|
||||
@ -55,221 +46,83 @@ npm i -g openclaw@latest
|
||||
pnpm add -g openclaw@latest
|
||||
```
|
||||
|
||||
We do **not** recommend Bun for the Gateway runtime (WhatsApp/Telegram bugs).
|
||||
## Auto-updater
|
||||
|
||||
To switch update channels (git + npm installs):
|
||||
The auto-updater is off by default. Enable it in `~/.openclaw/openclaw.json`:
|
||||
|
||||
```bash
|
||||
openclaw update --channel beta
|
||||
openclaw update --channel dev
|
||||
openclaw update --channel stable
|
||||
```
|
||||
|
||||
Use `--tag <dist-tag|version|spec>` for a one-off package target override.
|
||||
|
||||
For the current GitHub `main` head via a package-manager install:
|
||||
|
||||
```bash
|
||||
openclaw update --tag main
|
||||
```
|
||||
|
||||
Manual equivalents:
|
||||
|
||||
```bash
|
||||
npm i -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm add -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
You can also pass an explicit package spec to `--tag` for one-off updates (for example a GitHub ref or tarball URL).
|
||||
|
||||
See [Development channels](/install/development-channels) for channel semantics and release notes.
|
||||
|
||||
Note: on npm installs, the gateway logs an update hint on startup (checks the current channel tag). Disable via `update.checkOnStart: false`.
|
||||
|
||||
### Core auto-updater (optional)
|
||||
|
||||
Auto-updater is **off by default** and is a core Gateway feature (not a plugin).
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"update": {
|
||||
"channel": "stable",
|
||||
"auto": {
|
||||
"enabled": true,
|
||||
"stableDelayHours": 6,
|
||||
"stableJitterHours": 12,
|
||||
"betaCheckIntervalHours": 1
|
||||
}
|
||||
}
|
||||
update: {
|
||||
channel: "stable",
|
||||
auto: {
|
||||
enabled: true,
|
||||
stableDelayHours: 6,
|
||||
stableJitterHours: 12,
|
||||
betaCheckIntervalHours: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Behavior:
|
||||
| Channel | Behavior |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `stable` | Waits `stableDelayHours`, then applies with deterministic jitter across `stableJitterHours` (spread rollout). |
|
||||
| `beta` | Checks every `betaCheckIntervalHours` (default: hourly) and applies immediately. |
|
||||
| `dev` | No automatic apply. Use `openclaw update` manually. |
|
||||
|
||||
- `stable`: when a new version is seen, OpenClaw waits `stableDelayHours` and then applies a deterministic per-install jitter in `stableJitterHours` (spread rollout).
|
||||
- `beta`: checks on `betaCheckIntervalHours` cadence (default: hourly) and applies when an update is available.
|
||||
- `dev`: no automatic apply; use manual `openclaw update`.
|
||||
The gateway also logs an update hint on startup (disable with `update.checkOnStart: false`).
|
||||
|
||||
Use `openclaw update --dry-run` to preview update actions before enabling automation.
|
||||
## After updating
|
||||
|
||||
Then:
|
||||
<Steps>
|
||||
|
||||
### Run doctor
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
Migrates config, audits DM policies, and checks gateway health. Details: [Doctor](/gateway/doctor)
|
||||
|
||||
### Restart the gateway
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
openclaw health
|
||||
```
|
||||
|
||||
Notes:
|
||||
</Steps>
|
||||
|
||||
- If your Gateway runs as a service, `openclaw gateway restart` is preferred over killing PIDs.
|
||||
- If you’re pinned to a specific version, see “Rollback / pinning” below.
|
||||
## Rollback
|
||||
|
||||
## Update (`openclaw update`)
|
||||
|
||||
For **source installs** (git checkout), prefer:
|
||||
|
||||
```bash
|
||||
openclaw update
|
||||
```
|
||||
|
||||
It runs a safe-ish update flow:
|
||||
|
||||
- Requires a clean worktree.
|
||||
- Switches to the selected channel (tag or branch).
|
||||
- Fetches + rebases against the configured upstream (dev channel).
|
||||
- Installs deps, builds, builds the Control UI, and runs `openclaw doctor`.
|
||||
- Restarts the gateway by default (use `--no-restart` to skip).
|
||||
|
||||
If you installed via **npm/pnpm** (no git metadata), `openclaw update` will try to update via your package manager. If it can’t detect the install, use “Update (global install)” instead.
|
||||
|
||||
## Update (Control UI / RPC)
|
||||
|
||||
The Control UI has **Update & Restart** (RPC: `update.run`). It:
|
||||
|
||||
1. Runs the same source-update flow as `openclaw update` (git checkout only).
|
||||
2. Writes a restart sentinel with a structured report (stdout/stderr tail).
|
||||
3. Restarts the gateway and pings the last active session with the report.
|
||||
|
||||
If the rebase fails, the gateway aborts and restarts without applying the update.
|
||||
|
||||
## Update (from source)
|
||||
|
||||
From the repo checkout:
|
||||
|
||||
Preferred:
|
||||
|
||||
```bash
|
||||
openclaw update
|
||||
```
|
||||
|
||||
Manual (equivalent-ish):
|
||||
|
||||
```bash
|
||||
git pull
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm ui:build # auto-installs UI deps on first run
|
||||
openclaw doctor
|
||||
openclaw health
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `pnpm build` matters when you run the packaged `openclaw` binary ([`openclaw.mjs`](https://github.com/openclaw/openclaw/blob/main/openclaw.mjs)) or use Node to run `dist/`.
|
||||
- If you run from a repo checkout without a global install, use `pnpm openclaw ...` for CLI commands.
|
||||
- If you run directly from TypeScript (`pnpm openclaw ...`), a rebuild is usually unnecessary, but **config migrations still apply** → run doctor.
|
||||
- Switching between global and git installs is easy: install the other flavor, then run `openclaw doctor` so the gateway service entrypoint is rewritten to the current install.
|
||||
|
||||
## Always Run: `openclaw doctor`
|
||||
|
||||
Doctor is the “safe update” command. It’s intentionally boring: repair + migrate + warn.
|
||||
|
||||
Note: if you’re on a **source install** (git checkout), `openclaw doctor` will offer to run `openclaw update` first.
|
||||
|
||||
Typical things it does:
|
||||
|
||||
- Migrate deprecated config keys / legacy config file locations.
|
||||
- Audit DM policies and warn on risky “open” settings.
|
||||
- Check Gateway health and can offer to restart.
|
||||
- Detect and migrate older gateway services (launchd/systemd; legacy schtasks) to current OpenClaw services.
|
||||
- On Linux, ensure systemd user lingering (so the Gateway survives logout).
|
||||
|
||||
Details: [Doctor](/gateway/doctor)
|
||||
|
||||
## Start / stop / restart the Gateway
|
||||
|
||||
CLI (works regardless of OS):
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
openclaw gateway stop
|
||||
openclaw gateway restart
|
||||
openclaw gateway --port 18789
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
If you’re supervised:
|
||||
|
||||
- macOS launchd (app-bundled LaunchAgent): `launchctl kickstart -k gui/$UID/ai.openclaw.gateway` (use `ai.openclaw.<profile>`; legacy `com.openclaw.*` still works)
|
||||
- Linux systemd user service: `systemctl --user restart openclaw-gateway[-<profile>].service`
|
||||
- Windows (WSL2): `systemctl --user restart openclaw-gateway[-<profile>].service`
|
||||
- `launchctl`/`systemctl` only work if the service is installed; otherwise run `openclaw gateway install`.
|
||||
|
||||
Runbook + exact service labels: [Gateway runbook](/gateway)
|
||||
|
||||
## Rollback / pinning (when something breaks)
|
||||
|
||||
### Pin (global install)
|
||||
|
||||
Install a known-good version (replace `<version>` with the last working one):
|
||||
### Pin a version (npm)
|
||||
|
||||
```bash
|
||||
npm i -g openclaw@<version>
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm add -g openclaw@<version>
|
||||
```
|
||||
|
||||
Tip: to see the current published version, run `npm view openclaw version`.
|
||||
|
||||
Then restart + re-run doctor:
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
### Pin (source) by date
|
||||
Tip: `npm view openclaw version` shows the current published version.
|
||||
|
||||
Pick a commit from a date (example: “state of main as of 2026-01-01”):
|
||||
### Pin a commit (source)
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout "$(git rev-list -n 1 --before=\"2026-01-01\" origin/main)"
|
||||
```
|
||||
|
||||
Then reinstall deps + restart:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm install && pnpm build
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
If you want to go back to latest later:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull
|
||||
```
|
||||
To return to latest: `git checkout main && git pull`.
|
||||
|
||||
## If you are stuck
|
||||
|
||||
- Run `openclaw doctor` again and read the output carefully (it often tells you the fix).
|
||||
- Run `openclaw doctor` again and read the output carefully.
|
||||
- Check: [Troubleshooting](/gateway/troubleshooting)
|
||||
- Ask in Discord: [https://discord.gg/clawd](https://discord.gg/clawd)
|
||||
|
||||
@ -21,7 +21,7 @@ Native Linux companion apps are planned. Contributions are welcome if you want t
|
||||
4. From your laptop: `ssh -N -L 18789:127.0.0.1:18789 <user>@<host>`
|
||||
5. Open `http://127.0.0.1:18789/` and paste your token
|
||||
|
||||
Step-by-step VPS guide: [exe.dev](/install/exe-dev)
|
||||
Full Linux server guide: [Linux Server](/vps). Step-by-step VPS example: [exe.dev](/install/exe-dev)
|
||||
|
||||
## Install
|
||||
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
---
|
||||
summary: "Windows (WSL2) support + companion app status"
|
||||
summary: "Windows support: native and WSL2 install paths, daemon, and current caveats"
|
||||
read_when:
|
||||
- Installing OpenClaw on Windows
|
||||
- Choosing between native Windows and WSL2
|
||||
- Looking for Windows companion app status
|
||||
title: "Windows (WSL2)"
|
||||
title: "Windows"
|
||||
---
|
||||
|
||||
# Windows (WSL2)
|
||||
# Windows
|
||||
|
||||
OpenClaw on Windows is recommended **via WSL2** (Ubuntu recommended). The
|
||||
CLI + Gateway run inside Linux, which keeps the runtime consistent and makes
|
||||
tooling far more compatible (Node/Bun/pnpm, Linux binaries, skills). Native
|
||||
Windows might be trickier. WSL2 gives you the full Linux experience — one command
|
||||
to install: `wsl --install`.
|
||||
OpenClaw supports both **native Windows** and **WSL2**. WSL2 is the more
|
||||
stable path and recommended for the full experience — the CLI, Gateway, and
|
||||
tooling run inside Linux with full compatibility. Native Windows works for
|
||||
core CLI and Gateway use, with some caveats noted below.
|
||||
|
||||
Native Windows companion apps are planned.
|
||||
|
||||
## Install (WSL2)
|
||||
## WSL2 (recommended)
|
||||
|
||||
- [Getting Started](/start/getting-started) (use inside WSL)
|
||||
- [Install & updates](/install/updating)
|
||||
|
||||
@ -57,7 +57,7 @@ OpenClaw's shared `/fast` toggle also supports direct Anthropic API-key traffic.
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-5": {
|
||||
"anthropic/claude-sonnet-4-6": {
|
||||
params: { fastMode: true },
|
||||
},
|
||||
},
|
||||
@ -228,7 +228,7 @@ openclaw onboard --auth-choice setup-token
|
||||
## Notes
|
||||
|
||||
- Generate the setup-token with `claude setup-token` and paste it, or run `openclaw models auth setup-token` on the gateway host.
|
||||
- If you see “OAuth token refresh failed …” on a Claude subscription, re-auth with a setup-token. See [/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription](/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription).
|
||||
- If you see “OAuth token refresh failed …” on a Claude subscription, re-auth with a setup-token. See [/gateway/troubleshooting](/gateway/troubleshooting).
|
||||
- Auth details + reuse rules are in [/concepts/oauth](/concepts/oauth).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@ -12,7 +12,7 @@ Cloudflare AI Gateway sits in front of provider APIs and lets you add analytics,
|
||||
|
||||
- Provider: `cloudflare-ai-gateway`
|
||||
- Base URL: `https://gateway.ai.cloudflare.com/v1/<account_id>/<gateway_id>/anthropic`
|
||||
- Default model: `cloudflare-ai-gateway/claude-sonnet-4-5`
|
||||
- Default model: `cloudflare-ai-gateway/claude-sonnet-4-6`
|
||||
- API key: `CLOUDFLARE_AI_GATEWAY_API_KEY` (your provider API key for requests through the Gateway)
|
||||
|
||||
For Anthropic models, use your Anthropic API key.
|
||||
@ -31,7 +31,7 @@ openclaw onboard --auth-choice cloudflare-ai-gateway-api-key
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "cloudflare-ai-gateway/claude-sonnet-4-5" },
|
||||
model: { primary: "cloudflare-ai-gateway/claude-sonnet-4-6" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
96
docs/providers/groq.md
Normal file
96
docs/providers/groq.md
Normal file
@ -0,0 +1,96 @@
|
||||
---
|
||||
title: "Groq"
|
||||
summary: "Groq setup (auth + model selection)"
|
||||
read_when:
|
||||
- You want to use Groq with OpenClaw
|
||||
- You need the API key env var or CLI auth choice
|
||||
---
|
||||
|
||||
# Groq
|
||||
|
||||
[Groq](https://groq.com) provides ultra-fast inference on open-source models
|
||||
(Llama, Gemma, Mistral, and more) using custom LPU hardware. OpenClaw connects
|
||||
to Groq through its OpenAI-compatible API.
|
||||
|
||||
- Provider: `groq`
|
||||
- Auth: `GROQ_API_KEY`
|
||||
- API: OpenAI-compatible
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Get an API key from [console.groq.com/keys](https://console.groq.com/keys).
|
||||
|
||||
2. Set the API key:
|
||||
|
||||
```bash
|
||||
export GROQ_API_KEY="gsk_..."
|
||||
```
|
||||
|
||||
3. Set a default model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "groq/llama-3.3-70b-versatile" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Config file example
|
||||
|
||||
```json5
|
||||
{
|
||||
env: { GROQ_API_KEY: "gsk_..." },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "groq/llama-3.3-70b-versatile" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Audio transcription
|
||||
|
||||
Groq also provides fast Whisper-based audio transcription. When configured as a
|
||||
media-understanding provider, OpenClaw uses Groq's `whisper-large-v3-turbo`
|
||||
model to transcribe voice messages.
|
||||
|
||||
```json5
|
||||
{
|
||||
media: {
|
||||
understanding: {
|
||||
audio: {
|
||||
models: [{ provider: "groq" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Environment note
|
||||
|
||||
If the Gateway runs as a daemon (launchd/systemd), make sure `GROQ_API_KEY` is
|
||||
available to that process (for example, in `~/.openclaw/.env` or via
|
||||
`env.shellEnv`).
|
||||
|
||||
## Available models
|
||||
|
||||
Groq's model catalog changes frequently. Run `openclaw models list | grep groq`
|
||||
to see currently available models, or check
|
||||
[console.groq.com/docs/models](https://console.groq.com/docs/models).
|
||||
|
||||
Popular choices include:
|
||||
|
||||
- **Llama 3.3 70B Versatile** - general-purpose, large context
|
||||
- **Llama 3.1 8B Instant** - fast, lightweight
|
||||
- **Gemma 2 9B** - compact, efficient
|
||||
- **Mixtral 8x7B** - MoE architecture, strong reasoning
|
||||
|
||||
## Links
|
||||
|
||||
- [Groq Console](https://console.groq.com)
|
||||
- [API Documentation](https://console.groq.com/docs)
|
||||
- [Model List](https://console.groq.com/docs/models)
|
||||
- [Pricing](https://groq.com/pricing)
|
||||
@ -31,6 +31,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
|
||||
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
|
||||
- [GLM models](/providers/glm)
|
||||
- [Google (Gemini)](/providers/google)
|
||||
- [Groq (LPU inference)](/providers/groq)
|
||||
- [Hugging Face (Inference)](/providers/huggingface)
|
||||
- [Kilocode](/providers/kilocode)
|
||||
- [LiteLLM (unified gateway)](/providers/litellm)
|
||||
|
||||
@ -24,7 +24,7 @@ openclaw onboard --auth-choice apiKey --token-provider openrouter --token "$OPEN
|
||||
env: { OPENROUTER_API_KEY: "sk-or-..." },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openrouter/anthropic/claude-sonnet-4-5" },
|
||||
model: { primary: "openrouter/anthropic/claude-sonnet-4-6" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -13,19 +13,25 @@ Search API or Perplexity Sonar via OpenRouter.
|
||||
|
||||
<Note>
|
||||
This page covers the Perplexity **provider** setup. For the Perplexity
|
||||
**tool** (how the agent uses it), see [Perplexity tool](/perplexity).
|
||||
**tool** (how the agent uses it), see [Perplexity tool](/tools/perplexity-search).
|
||||
</Note>
|
||||
|
||||
- Type: web search provider (not a model provider)
|
||||
- Auth: `PERPLEXITY_API_KEY` (direct) or `OPENROUTER_API_KEY` (via OpenRouter)
|
||||
- Config path: `tools.web.search.perplexity.apiKey`
|
||||
- Config path: `plugins.entries.perplexity.config.webSearch.apiKey`
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Set the API key:
|
||||
|
||||
```bash
|
||||
openclaw config set tools.web.search.perplexity.apiKey "pplx-xxxxxxxxxxxx"
|
||||
openclaw configure --section web
|
||||
```
|
||||
|
||||
Or set it directly:
|
||||
|
||||
```bash
|
||||
openclaw config set plugins.entries.perplexity.config.webSearch.apiKey "pplx-xxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
2. The agent will automatically use Perplexity for web searches when configured.
|
||||
|
||||
711
docs/reference/memory-config.md
Normal file
711
docs/reference/memory-config.md
Normal file
@ -0,0 +1,711 @@
|
||||
---
|
||||
title: "Memory configuration reference"
|
||||
summary: "Full configuration reference for OpenClaw memory search, embedding providers, QMD backend, hybrid search, and multimodal memory"
|
||||
read_when:
|
||||
- You want to configure memory search providers or embedding models
|
||||
- You want to set up the QMD backend
|
||||
- You want to tune hybrid search, MMR, or temporal decay
|
||||
- You want to enable multimodal memory indexing
|
||||
---
|
||||
|
||||
# Memory configuration reference
|
||||
|
||||
This page covers the full configuration surface for OpenClaw memory search. For
|
||||
the conceptual overview (file layout, memory tools, when to write memory, and the
|
||||
automatic flush), see [Memory](/concepts/memory).
|
||||
|
||||
## Memory search defaults
|
||||
|
||||
- Enabled by default.
|
||||
- Watches memory files for changes (debounced).
|
||||
- Configure memory search under `agents.defaults.memorySearch` (not top-level
|
||||
`memorySearch`).
|
||||
- Uses remote embeddings by default. If `memorySearch.provider` is not set, OpenClaw auto-selects:
|
||||
1. `local` if a `memorySearch.local.modelPath` is configured and the file exists.
|
||||
2. `openai` if an OpenAI key can be resolved.
|
||||
3. `gemini` if a Gemini key can be resolved.
|
||||
4. `voyage` if a Voyage key can be resolved.
|
||||
5. `mistral` if a Mistral key can be resolved.
|
||||
6. Otherwise memory search stays disabled until configured.
|
||||
- Local mode uses node-llama-cpp and may require `pnpm approve-builds`.
|
||||
- Uses sqlite-vec (when available) to accelerate vector search inside SQLite.
|
||||
- `memorySearch.provider = "ollama"` is also supported for local/self-hosted
|
||||
Ollama embeddings (`/api/embeddings`), but it is not auto-selected.
|
||||
|
||||
Remote embeddings **require** an API key for the embedding provider. OpenClaw
|
||||
resolves keys from auth profiles, `models.providers.*.apiKey`, or environment
|
||||
variables. Codex OAuth only covers chat/completions and does **not** satisfy
|
||||
embeddings for memory search. For Gemini, use `GEMINI_API_KEY` or
|
||||
`models.providers.google.apiKey`. For Voyage, use `VOYAGE_API_KEY` or
|
||||
`models.providers.voyage.apiKey`. For Mistral, use `MISTRAL_API_KEY` or
|
||||
`models.providers.mistral.apiKey`. Ollama typically does not require a real API
|
||||
key (a placeholder like `OLLAMA_API_KEY=ollama-local` is enough when needed by
|
||||
local policy).
|
||||
When using a custom OpenAI-compatible endpoint,
|
||||
set `memorySearch.remote.apiKey` (and optional `memorySearch.remote.headers`).
|
||||
|
||||
## QMD backend (experimental)
|
||||
|
||||
Set `memory.backend = "qmd"` to swap the built-in SQLite indexer for
|
||||
[QMD](https://github.com/tobi/qmd): a local-first search sidecar that combines
|
||||
BM25 + vectors + reranking. Markdown stays the source of truth; OpenClaw shells
|
||||
out to QMD for retrieval. Key points:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Disabled by default. Opt in per-config (`memory.backend = "qmd"`).
|
||||
- Install the QMD CLI separately (`bun install -g https://github.com/tobi/qmd` or grab
|
||||
a release) and make sure the `qmd` binary is on the gateway's `PATH`.
|
||||
- QMD needs an SQLite build that allows extensions (`brew install sqlite` on
|
||||
macOS).
|
||||
- QMD runs fully locally via Bun + `node-llama-cpp` and auto-downloads GGUF
|
||||
models from HuggingFace on first use (no separate Ollama daemon required).
|
||||
- The gateway runs QMD in a self-contained XDG home under
|
||||
`~/.openclaw/agents/<agentId>/qmd/` by setting `XDG_CONFIG_HOME` and
|
||||
`XDG_CACHE_HOME`.
|
||||
- OS support: macOS and Linux work out of the box once Bun + SQLite are
|
||||
installed. Windows is best supported via WSL2.
|
||||
|
||||
### How the sidecar runs
|
||||
|
||||
- The gateway writes a self-contained QMD home under
|
||||
`~/.openclaw/agents/<agentId>/qmd/` (config + cache + sqlite DB).
|
||||
- Collections are created via `qmd collection add` from `memory.qmd.paths`
|
||||
(plus default workspace memory files), then `qmd update` + `qmd embed` run
|
||||
on boot and on a configurable interval (`memory.qmd.update.interval`,
|
||||
default 5 m).
|
||||
- The gateway now initializes the QMD manager on startup, so periodic update
|
||||
timers are armed even before the first `memory_search` call.
|
||||
- Boot refresh now runs in the background by default so chat startup is not
|
||||
blocked; set `memory.qmd.update.waitForBootSync = true` to keep the previous
|
||||
blocking behavior.
|
||||
- Searches run via `memory.qmd.searchMode` (default `qmd search --json`; also
|
||||
supports `vsearch` and `query`). If the selected mode rejects flags on your
|
||||
QMD build, OpenClaw retries with `qmd query`. If QMD fails or the binary is
|
||||
missing, OpenClaw automatically falls back to the builtin SQLite manager so
|
||||
memory tools keep working.
|
||||
- OpenClaw does not expose QMD embed batch-size tuning today; batch behavior is
|
||||
controlled by QMD itself.
|
||||
- **First search may be slow**: QMD may download local GGUF models (reranker/query
|
||||
expansion) on the first `qmd query` run.
|
||||
- OpenClaw sets `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` automatically when it runs QMD.
|
||||
- If you want to pre-download models manually (and warm the same index OpenClaw
|
||||
uses), run a one-off query with the agent's XDG dirs.
|
||||
|
||||
OpenClaw's QMD state lives under your **state dir** (defaults to `~/.openclaw`).
|
||||
You can point `qmd` at the exact same index by exporting the same XDG vars
|
||||
OpenClaw uses:
|
||||
|
||||
```bash
|
||||
# Pick the same state dir OpenClaw uses
|
||||
STATE_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
|
||||
|
||||
export XDG_CONFIG_HOME="$STATE_DIR/agents/main/qmd/xdg-config"
|
||||
export XDG_CACHE_HOME="$STATE_DIR/agents/main/qmd/xdg-cache"
|
||||
|
||||
# (Optional) force an index refresh + embeddings
|
||||
qmd update
|
||||
qmd embed
|
||||
|
||||
# Warm up / trigger first-time model downloads
|
||||
qmd query "test" -c memory-root --json >/dev/null 2>&1
|
||||
```
|
||||
|
||||
### Config surface (`memory.qmd.*`)
|
||||
|
||||
- `command` (default `qmd`): override the executable path.
|
||||
- `searchMode` (default `search`): pick which QMD command backs
|
||||
`memory_search` (`search`, `vsearch`, `query`).
|
||||
- `includeDefaultMemory` (default `true`): auto-index `MEMORY.md` + `memory/**/*.md`.
|
||||
- `paths[]`: add extra directories/files (`path`, optional `pattern`, optional
|
||||
stable `name`).
|
||||
- `sessions`: opt into session JSONL indexing (`enabled`, `retentionDays`,
|
||||
`exportDir`).
|
||||
- `update`: controls refresh cadence and maintenance execution:
|
||||
(`interval`, `debounceMs`, `onBoot`, `waitForBootSync`, `embedInterval`,
|
||||
`commandTimeoutMs`, `updateTimeoutMs`, `embedTimeoutMs`).
|
||||
- `limits`: clamp recall payload (`maxResults`, `maxSnippetChars`,
|
||||
`maxInjectedChars`, `timeoutMs`).
|
||||
- `scope`: same schema as [`session.sendPolicy`](/gateway/configuration-reference#session).
|
||||
Default is DM-only (`deny` all, `allow` direct chats); loosen it to surface QMD
|
||||
hits in groups/channels.
|
||||
- `match.keyPrefix` matches the **normalized** session key (lowercased, with any
|
||||
leading `agent:<id>:` stripped). Example: `discord:channel:`.
|
||||
- `match.rawKeyPrefix` matches the **raw** session key (lowercased), including
|
||||
`agent:<id>:`. Example: `agent:main:discord:`.
|
||||
- Legacy: `match.keyPrefix: "agent:..."` is still treated as a raw-key prefix,
|
||||
but prefer `rawKeyPrefix` for clarity.
|
||||
- When `scope` denies a search, OpenClaw logs a warning with the derived
|
||||
`channel`/`chatType` so empty results are easier to debug.
|
||||
- Snippets sourced outside the workspace show up as
|
||||
`qmd/<collection>/<relative-path>` in `memory_search` results; `memory_get`
|
||||
understands that prefix and reads from the configured QMD collection root.
|
||||
- When `memory.qmd.sessions.enabled = true`, OpenClaw exports sanitized session
|
||||
transcripts (User/Assistant turns) into a dedicated QMD collection under
|
||||
`~/.openclaw/agents/<id>/qmd/sessions/`, so `memory_search` can recall recent
|
||||
conversations without touching the builtin SQLite index.
|
||||
- `memory_search` snippets now include a `Source: <path#line>` footer when
|
||||
`memory.citations` is `auto`/`on`; set `memory.citations = "off"` to keep
|
||||
the path metadata internal (the agent still receives the path for
|
||||
`memory_get`, but the snippet text omits the footer and the system prompt
|
||||
warns the agent not to cite it).
|
||||
|
||||
### QMD example
|
||||
|
||||
```json5
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
citations: "auto",
|
||||
qmd: {
|
||||
includeDefaultMemory: true,
|
||||
update: { interval: "5m", debounceMs: 15000 },
|
||||
limits: { maxResults: 6, timeoutMs: 4000 },
|
||||
scope: {
|
||||
default: "deny",
|
||||
rules: [
|
||||
{ action: "allow", match: { chatType: "direct" } },
|
||||
// Normalized session-key prefix (strips `agent:<id>:`).
|
||||
{ action: "deny", match: { keyPrefix: "discord:channel:" } },
|
||||
// Raw session-key prefix (includes `agent:<id>:`).
|
||||
{ action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } },
|
||||
]
|
||||
},
|
||||
paths: [
|
||||
{ name: "docs", path: "~/notes", pattern: "**/*.md" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Citations and fallback
|
||||
|
||||
- `memory.citations` applies regardless of backend (`auto`/`on`/`off`).
|
||||
- When `qmd` runs, we tag `status().backend = "qmd"` so diagnostics show which
|
||||
engine served the results. If the QMD subprocess exits or JSON output can't be
|
||||
parsed, the search manager logs a warning and returns the builtin provider
|
||||
(existing Markdown embeddings) until QMD recovers.
|
||||
|
||||
## Additional memory paths
|
||||
|
||||
If you want to index Markdown files outside the default workspace layout, add
|
||||
explicit paths:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Paths can be absolute or workspace-relative.
|
||||
- Directories are scanned recursively for `.md` files.
|
||||
- By default, only Markdown files are indexed.
|
||||
- If `memorySearch.multimodal.enabled = true`, OpenClaw also indexes supported image/audio files under `extraPaths` only. Default memory roots (`MEMORY.md`, `memory.md`, `memory/**/*.md`) stay Markdown-only.
|
||||
- Symlinks are ignored (files or directories).
|
||||
|
||||
## Multimodal memory files (Gemini image + audio)
|
||||
|
||||
OpenClaw can index image and audio files from `memorySearch.extraPaths` when using Gemini embedding 2:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "gemini",
|
||||
model: "gemini-embedding-2-preview",
|
||||
extraPaths: ["assets/reference", "voice-notes"],
|
||||
multimodal: {
|
||||
enabled: true,
|
||||
modalities: ["image", "audio"], // or ["all"]
|
||||
maxFileBytes: 10000000
|
||||
},
|
||||
remote: {
|
||||
apiKey: "YOUR_GEMINI_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Multimodal memory is currently supported only for `gemini-embedding-2-preview`.
|
||||
- Multimodal indexing applies only to files discovered through `memorySearch.extraPaths`.
|
||||
- Supported modalities in this phase: image and audio.
|
||||
- `memorySearch.fallback` must stay `"none"` while multimodal memory is enabled.
|
||||
- Matching image/audio file bytes are uploaded to the configured Gemini embedding endpoint during indexing.
|
||||
- Supported image extensions: `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.heic`, `.heif`.
|
||||
- Supported audio extensions: `.mp3`, `.wav`, `.ogg`, `.opus`, `.m4a`, `.aac`, `.flac`.
|
||||
- Search queries remain text, but Gemini can compare those text queries against indexed image/audio embeddings.
|
||||
- `memory_get` still reads Markdown only; binary files are searchable but not returned as raw file contents.
|
||||
|
||||
## Gemini embeddings (native)
|
||||
|
||||
Set the provider to `gemini` to use the Gemini embeddings API directly:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "gemini",
|
||||
model: "gemini-embedding-001",
|
||||
remote: {
|
||||
apiKey: "YOUR_GEMINI_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `remote.baseUrl` is optional (defaults to the Gemini API base URL).
|
||||
- `remote.headers` lets you add extra headers if needed.
|
||||
- Default model: `gemini-embedding-001`.
|
||||
- `gemini-embedding-2-preview` is also supported: 8192 token limit and configurable dimensions (768 / 1536 / 3072, default 3072).
|
||||
|
||||
### Gemini Embedding 2 (preview)
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "gemini",
|
||||
model: "gemini-embedding-2-preview",
|
||||
outputDimensionality: 3072, // optional: 768, 1536, or 3072 (default)
|
||||
remote: {
|
||||
apiKey: "YOUR_GEMINI_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Re-index required:** Switching from `gemini-embedding-001` (768 dimensions)
|
||||
> to `gemini-embedding-2-preview` (3072 dimensions) changes the vector size. The same is true if you
|
||||
> change `outputDimensionality` between 768, 1536, and 3072.
|
||||
> OpenClaw will automatically reindex when it detects a model or dimension change.
|
||||
|
||||
## Custom OpenAI-compatible endpoint
|
||||
|
||||
If you want to use a custom OpenAI-compatible endpoint (OpenRouter, vLLM, or a proxy),
|
||||
you can use the `remote` configuration with the OpenAI provider:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "openai",
|
||||
model: "text-embedding-3-small",
|
||||
remote: {
|
||||
baseUrl: "https://api.example.com/v1/",
|
||||
apiKey: "YOUR_OPENAI_COMPAT_API_KEY",
|
||||
headers: { "X-Custom-Header": "value" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you don't want to set an API key, use `memorySearch.provider = "local"` or set
|
||||
`memorySearch.fallback = "none"`.
|
||||
|
||||
### Fallbacks
|
||||
|
||||
- `memorySearch.fallback` can be `openai`, `gemini`, `voyage`, `mistral`, `ollama`, `local`, or `none`.
|
||||
- The fallback provider is only used when the primary embedding provider fails.
|
||||
|
||||
### Batch indexing (OpenAI + Gemini + Voyage)
|
||||
|
||||
- Disabled by default. Set `agents.defaults.memorySearch.remote.batch.enabled = true` to enable for large-corpus indexing (OpenAI, Gemini, and Voyage).
|
||||
- Default behavior waits for batch completion; tune `remote.batch.wait`, `remote.batch.pollIntervalMs`, and `remote.batch.timeoutMinutes` if needed.
|
||||
- Set `remote.batch.concurrency` to control how many batch jobs we submit in parallel (default: 2).
|
||||
- Batch mode applies when `memorySearch.provider = "openai"` or `"gemini"` and uses the corresponding API key.
|
||||
- Gemini batch jobs use the async embeddings batch endpoint and require Gemini Batch API availability.
|
||||
|
||||
Why OpenAI batch is fast and cheap:
|
||||
|
||||
- For large backfills, OpenAI is typically the fastest option we support because we can submit many embedding requests in a single batch job and let OpenAI process them asynchronously.
|
||||
- OpenAI offers discounted pricing for Batch API workloads, so large indexing runs are usually cheaper than sending the same requests synchronously.
|
||||
- See the OpenAI Batch API docs and pricing for details:
|
||||
- [https://platform.openai.com/docs/api-reference/batch](https://platform.openai.com/docs/api-reference/batch)
|
||||
- [https://platform.openai.com/pricing](https://platform.openai.com/pricing)
|
||||
|
||||
Config example:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "openai",
|
||||
model: "text-embedding-3-small",
|
||||
fallback: "openai",
|
||||
remote: {
|
||||
batch: { enabled: true, concurrency: 2 }
|
||||
},
|
||||
sync: { watch: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How the memory tools work
|
||||
|
||||
- `memory_search` semantically searches Markdown chunks (~400 token target, 80-token overlap) from `MEMORY.md` + `memory/**/*.md`. It returns snippet text (capped ~700 chars), file path, line range, score, provider/model, and whether we fell back from local to remote embeddings. No full file payload is returned.
|
||||
- `memory_get` reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are rejected.
|
||||
- Both tools are enabled only when `memorySearch.enabled` resolves true for the agent.
|
||||
|
||||
## What gets indexed (and when)
|
||||
|
||||
- File type: Markdown only (`MEMORY.md`, `memory/**/*.md`).
|
||||
- Index storage: per-agent SQLite at `~/.openclaw/memory/<agentId>.sqlite` (configurable via `agents.defaults.memorySearch.store.path`, supports `{agentId}` token).
|
||||
- Freshness: watcher on `MEMORY.md` + `memory/` marks the index dirty (debounce 1.5s). Sync is scheduled on session start, on search, or on an interval and runs asynchronously. Session transcripts use delta thresholds to trigger background sync.
|
||||
- Reindex triggers: the index stores the embedding **provider/model + endpoint fingerprint + chunking params**. If any of those change, OpenClaw automatically resets and reindexes the entire store.
|
||||
|
||||
## Hybrid search (BM25 + vector)
|
||||
|
||||
When enabled, OpenClaw combines:
|
||||
|
||||
- **Vector similarity** (semantic match, wording can differ)
|
||||
- **BM25 keyword relevance** (exact tokens like IDs, env vars, code symbols)
|
||||
|
||||
If full-text search is unavailable on your platform, OpenClaw falls back to vector-only search.
|
||||
|
||||
### Why hybrid
|
||||
|
||||
Vector search is great at "this means the same thing":
|
||||
|
||||
- "Mac Studio gateway host" vs "the machine running the gateway"
|
||||
- "debounce file updates" vs "avoid indexing on every write"
|
||||
|
||||
But it can be weak at exact, high-signal tokens:
|
||||
|
||||
- IDs (`a828e60`, `b3b9895a...`)
|
||||
- code symbols (`memorySearch.query.hybrid`)
|
||||
- error strings ("sqlite-vec unavailable")
|
||||
|
||||
BM25 (full-text) is the opposite: strong at exact tokens, weaker at paraphrases.
|
||||
Hybrid search is the pragmatic middle ground: **use both retrieval signals** so you get
|
||||
good results for both "natural language" queries and "needle in a haystack" queries.
|
||||
|
||||
### How we merge results (the current design)
|
||||
|
||||
Implementation sketch:
|
||||
|
||||
1. Retrieve a candidate pool from both sides:
|
||||
|
||||
- **Vector**: top `maxResults * candidateMultiplier` by cosine similarity.
|
||||
- **BM25**: top `maxResults * candidateMultiplier` by FTS5 BM25 rank (lower is better).
|
||||
|
||||
2. Convert BM25 rank into a 0..1-ish score:
|
||||
|
||||
- `textScore = 1 / (1 + max(0, bm25Rank))`
|
||||
|
||||
3. Union candidates by chunk id and compute a weighted score:
|
||||
|
||||
- `finalScore = vectorWeight * vectorScore + textWeight * textScore`
|
||||
|
||||
Notes:
|
||||
|
||||
- `vectorWeight` + `textWeight` is normalized to 1.0 in config resolution, so weights behave as percentages.
|
||||
- If embeddings are unavailable (or the provider returns a zero-vector), we still run BM25 and return keyword matches.
|
||||
- If FTS5 can't be created, we keep vector-only search (no hard failure).
|
||||
|
||||
This isn't "IR-theory perfect", but it's simple, fast, and tends to improve recall/precision on real notes.
|
||||
If we want to get fancier later, common next steps are Reciprocal Rank Fusion (RRF) or score normalization
|
||||
(min/max or z-score) before mixing.
|
||||
|
||||
### Post-processing pipeline
|
||||
|
||||
After merging vector and keyword scores, two optional post-processing stages
|
||||
refine the result list before it reaches the agent:
|
||||
|
||||
```
|
||||
Vector + Keyword -> Weighted Merge -> Temporal Decay -> Sort -> MMR -> Top-K Results
|
||||
```
|
||||
|
||||
Both stages are **off by default** and can be enabled independently.
|
||||
|
||||
### MMR re-ranking (diversity)
|
||||
|
||||
When hybrid search returns results, multiple chunks may contain similar or overlapping content.
|
||||
For example, searching for "home network setup" might return five nearly identical snippets
|
||||
from different daily notes that all mention the same router configuration.
|
||||
|
||||
**MMR (Maximal Marginal Relevance)** re-ranks the results to balance relevance with diversity,
|
||||
ensuring the top results cover different aspects of the query instead of repeating the same information.
|
||||
|
||||
How it works:
|
||||
|
||||
1. Results are scored by their original relevance (vector + BM25 weighted score).
|
||||
2. MMR iteratively selects results that maximize: `lambda x relevance - (1-lambda) x max_similarity_to_selected`.
|
||||
3. Similarity between results is measured using Jaccard text similarity on tokenized content.
|
||||
|
||||
The `lambda` parameter controls the trade-off:
|
||||
|
||||
- `lambda = 1.0` -- pure relevance (no diversity penalty)
|
||||
- `lambda = 0.0` -- maximum diversity (ignores relevance)
|
||||
- Default: `0.7` (balanced, slight relevance bias)
|
||||
|
||||
**Example -- query: "home network setup"**
|
||||
|
||||
Given these memory files:
|
||||
|
||||
```
|
||||
memory/2026-02-10.md -> "Configured Omada router, set VLAN 10 for IoT devices"
|
||||
memory/2026-02-08.md -> "Configured Omada router, moved IoT to VLAN 10"
|
||||
memory/2026-02-05.md -> "Set up AdGuard DNS on 192.168.10.2"
|
||||
memory/network.md -> "Router: Omada ER605, AdGuard: 192.168.10.2, VLAN 10: IoT"
|
||||
```
|
||||
|
||||
Without MMR -- top 3 results:
|
||||
|
||||
```
|
||||
1. memory/2026-02-10.md (score: 0.92) <- router + VLAN
|
||||
2. memory/2026-02-08.md (score: 0.89) <- router + VLAN (near-duplicate!)
|
||||
3. memory/network.md (score: 0.85) <- reference doc
|
||||
```
|
||||
|
||||
With MMR (lambda=0.7) -- top 3 results:
|
||||
|
||||
```
|
||||
1. memory/2026-02-10.md (score: 0.92) <- router + VLAN
|
||||
2. memory/network.md (score: 0.85) <- reference doc (diverse!)
|
||||
3. memory/2026-02-05.md (score: 0.78) <- AdGuard DNS (diverse!)
|
||||
```
|
||||
|
||||
The near-duplicate from Feb 8 drops out, and the agent gets three distinct pieces of information.
|
||||
|
||||
**When to enable:** If you notice `memory_search` returning redundant or near-duplicate snippets,
|
||||
especially with daily notes that often repeat similar information across days.
|
||||
|
||||
### Temporal decay (recency boost)
|
||||
|
||||
Agents with daily notes accumulate hundreds of dated files over time. Without decay,
|
||||
a well-worded note from six months ago can outrank yesterday's update on the same topic.
|
||||
|
||||
**Temporal decay** applies an exponential multiplier to scores based on the age of each result,
|
||||
so recent memories naturally rank higher while old ones fade:
|
||||
|
||||
```
|
||||
decayedScore = score x e^(-lambda x ageInDays)
|
||||
```
|
||||
|
||||
where `lambda = ln(2) / halfLifeDays`.
|
||||
|
||||
With the default half-life of 30 days:
|
||||
|
||||
- Today's notes: **100%** of original score
|
||||
- 7 days ago: **~84%**
|
||||
- 30 days ago: **50%**
|
||||
- 90 days ago: **12.5%**
|
||||
- 180 days ago: **~1.6%**
|
||||
|
||||
**Evergreen files are never decayed:**
|
||||
|
||||
- `MEMORY.md` (root memory file)
|
||||
- Non-dated files in `memory/` (e.g., `memory/projects.md`, `memory/network.md`)
|
||||
- These contain durable reference information that should always rank normally.
|
||||
|
||||
**Dated daily files** (`memory/YYYY-MM-DD.md`) use the date extracted from the filename.
|
||||
Other sources (e.g., session transcripts) fall back to file modification time (`mtime`).
|
||||
|
||||
**Example -- query: "what's Rod's work schedule?"**
|
||||
|
||||
Given these memory files (today is Feb 10):
|
||||
|
||||
```
|
||||
memory/2025-09-15.md -> "Rod works Mon-Fri, standup at 10am, pairing at 2pm" (148 days old)
|
||||
memory/2026-02-10.md -> "Rod has standup at 14:15, 1:1 with Zeb at 14:45" (today)
|
||||
memory/2026-02-03.md -> "Rod started new team, standup moved to 14:15" (7 days old)
|
||||
```
|
||||
|
||||
Without decay:
|
||||
|
||||
```
|
||||
1. memory/2025-09-15.md (score: 0.91) <- best semantic match, but stale!
|
||||
2. memory/2026-02-10.md (score: 0.82)
|
||||
3. memory/2026-02-03.md (score: 0.80)
|
||||
```
|
||||
|
||||
With decay (halfLife=30):
|
||||
|
||||
```
|
||||
1. memory/2026-02-10.md (score: 0.82 x 1.00 = 0.82) <- today, no decay
|
||||
2. memory/2026-02-03.md (score: 0.80 x 0.85 = 0.68) <- 7 days, mild decay
|
||||
3. memory/2025-09-15.md (score: 0.91 x 0.03 = 0.03) <- 148 days, nearly gone
|
||||
```
|
||||
|
||||
The stale September note drops to the bottom despite having the best raw semantic match.
|
||||
|
||||
**When to enable:** If your agent has months of daily notes and you find that old,
|
||||
stale information outranks recent context. A half-life of 30 days works well for
|
||||
daily-note-heavy workflows; increase it (e.g., 90 days) if you reference older notes frequently.
|
||||
|
||||
### Hybrid search configuration
|
||||
|
||||
Both features are configured under `memorySearch.query.hybrid`:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
query: {
|
||||
hybrid: {
|
||||
enabled: true,
|
||||
vectorWeight: 0.7,
|
||||
textWeight: 0.3,
|
||||
candidateMultiplier: 4,
|
||||
// Diversity: reduce redundant results
|
||||
mmr: {
|
||||
enabled: true, // default: false
|
||||
lambda: 0.7 // 0 = max diversity, 1 = max relevance
|
||||
},
|
||||
// Recency: boost newer memories
|
||||
temporalDecay: {
|
||||
enabled: true, // default: false
|
||||
halfLifeDays: 30 // score halves every 30 days
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can enable either feature independently:
|
||||
|
||||
- **MMR only** -- useful when you have many similar notes but age doesn't matter.
|
||||
- **Temporal decay only** -- useful when recency matters but your results are already diverse.
|
||||
- **Both** -- recommended for agents with large, long-running daily note histories.
|
||||
|
||||
## Embedding cache
|
||||
|
||||
OpenClaw can cache **chunk embeddings** in SQLite so reindexing and frequent updates (especially session transcripts) don't re-embed unchanged text.
|
||||
|
||||
Config:
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
cache: {
|
||||
enabled: true,
|
||||
maxEntries: 50000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Session memory search (experimental)
|
||||
|
||||
You can optionally index **session transcripts** and surface them via `memory_search`.
|
||||
This is gated behind an experimental flag.
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
experimental: { sessionMemory: true },
|
||||
sources: ["memory", "sessions"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Session indexing is **opt-in** (off by default).
|
||||
- Session updates are debounced and **indexed asynchronously** once they cross delta thresholds (best-effort).
|
||||
- `memory_search` never blocks on indexing; results can be slightly stale until background sync finishes.
|
||||
- Results still include snippets only; `memory_get` remains limited to memory files.
|
||||
- Session indexing is isolated per agent (only that agent's session logs are indexed).
|
||||
- Session logs live on disk (`~/.openclaw/agents/<agentId>/sessions/*.jsonl`). Any process/user with filesystem access can read them, so treat disk access as the trust boundary. For stricter isolation, run agents under separate OS users or hosts.
|
||||
|
||||
Delta thresholds (defaults shown):
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
sync: {
|
||||
sessions: {
|
||||
deltaBytes: 100000, // ~100 KB
|
||||
deltaMessages: 50 // JSONL lines
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SQLite vector acceleration (sqlite-vec)
|
||||
|
||||
When the sqlite-vec extension is available, OpenClaw stores embeddings in a
|
||||
SQLite virtual table (`vec0`) and performs vector distance queries in the
|
||||
database. This keeps search fast without loading every embedding into JS.
|
||||
|
||||
Configuration (optional):
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
store: {
|
||||
vector: {
|
||||
enabled: true,
|
||||
extensionPath: "/path/to/sqlite-vec"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `enabled` defaults to true; when disabled, search falls back to in-process
|
||||
cosine similarity over stored embeddings.
|
||||
- If the sqlite-vec extension is missing or fails to load, OpenClaw logs the
|
||||
error and continues with the JS fallback (no vector table).
|
||||
- `extensionPath` overrides the bundled sqlite-vec path (useful for custom builds
|
||||
or non-standard install locations).
|
||||
|
||||
## Local embedding auto-download
|
||||
|
||||
- Default local embedding model: `hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf` (~0.6 GB).
|
||||
- When `memorySearch.provider = "local"`, `node-llama-cpp` resolves `modelPath`; if the GGUF is missing it **auto-downloads** to the cache (or `local.modelCacheDir` if set), then loads it. Downloads resume on retry.
|
||||
- Native build requirement: run `pnpm approve-builds`, pick `node-llama-cpp`, then `pnpm rebuild node-llama-cpp`.
|
||||
- Fallback: if local setup fails and `memorySearch.fallback = "openai"`, we automatically switch to remote embeddings (`openai/text-embedding-3-small` unless overridden) and record the reason.
|
||||
|
||||
## Custom OpenAI-compatible endpoint example
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
provider: "openai",
|
||||
model: "text-embedding-3-small",
|
||||
remote: {
|
||||
baseUrl: "https://api.example.com/v1/",
|
||||
apiKey: "YOUR_REMOTE_API_KEY",
|
||||
headers: {
|
||||
"X-Organization": "org-id",
|
||||
"X-Project": "project-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `remote.*` takes precedence over `models.providers.openai.*`.
|
||||
- `remote.headers` merge with OpenAI headers; remote wins on key conflicts. Omit `remote.headers` to use the OpenAI defaults.
|
||||
@ -8,29 +8,28 @@ title: "Getting Started"
|
||||
|
||||
# Getting Started
|
||||
|
||||
Goal: go from zero to a first working chat with minimal setup.
|
||||
Install OpenClaw, run onboarding, and chat with your AI assistant — all in
|
||||
about 5 minutes. By the end you will have a running Gateway, configured auth,
|
||||
and a working chat session.
|
||||
|
||||
<Info>
|
||||
Fastest chat: open the Control UI (no channel setup needed). Run `openclaw dashboard`
|
||||
and chat in the browser, or open `http://127.0.0.1:18789/` on the
|
||||
<Tooltip headline="Gateway host" tip="The machine running the OpenClaw gateway service.">gateway host</Tooltip>.
|
||||
Docs: [Dashboard](/web/dashboard) and [Control UI](/web/control-ui).
|
||||
</Info>
|
||||
## What you need
|
||||
|
||||
## Prereqs
|
||||
|
||||
- Node 24 recommended (Node 22 LTS, currently `22.16+`, still supported for compatibility)
|
||||
- **Node.js** — Node 24 recommended (Node 22.16+ also supported)
|
||||
- **An API key** from a model provider (Anthropic, OpenAI, Google, etc.) — onboarding will prompt you
|
||||
|
||||
<Tip>
|
||||
Check your Node version with `node --version` if you are unsure.
|
||||
Check your Node version with `node --version`.
|
||||
**Windows users:** both native Windows and WSL2 are supported. WSL2 is more
|
||||
stable and recommended for the full experience. See [Windows](/platforms/windows).
|
||||
Need to install Node? See [Node setup](/install/node).
|
||||
</Tip>
|
||||
|
||||
## Quick setup (CLI)
|
||||
## Quick setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Install OpenClaw (recommended)">
|
||||
<Step title="Install OpenClaw">
|
||||
<Tabs>
|
||||
<Tab title="macOS/Linux">
|
||||
<Tab title="macOS / Linux">
|
||||
```bash
|
||||
curl -fsSL https://openclaw.ai/install.sh | bash
|
||||
```
|
||||
@ -48,7 +47,7 @@ Check your Node version with `node --version` if you are unsure.
|
||||
</Tabs>
|
||||
|
||||
<Note>
|
||||
Other install methods and requirements: [Install](/install).
|
||||
Other install methods (Docker, Nix, npm): [Install](/install).
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
@ -57,79 +56,61 @@ Check your Node version with `node --version` if you are unsure.
|
||||
openclaw onboard --install-daemon
|
||||
```
|
||||
|
||||
Onboarding configures auth, gateway settings, and optional channels.
|
||||
See [Onboarding (CLI)](/start/wizard) for details.
|
||||
The wizard walks you through choosing a model provider, setting an API key,
|
||||
and configuring the Gateway. It takes about 2 minutes.
|
||||
|
||||
See [Onboarding (CLI)](/start/wizard) for the full reference.
|
||||
|
||||
</Step>
|
||||
<Step title="Check the Gateway">
|
||||
If you installed the service, it should already be running:
|
||||
|
||||
<Step title="Verify the Gateway is running">
|
||||
```bash
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
You should see the Gateway listening on port 18789.
|
||||
|
||||
</Step>
|
||||
<Step title="Open the Control UI">
|
||||
<Step title="Open the dashboard">
|
||||
```bash
|
||||
openclaw dashboard
|
||||
```
|
||||
|
||||
This opens the Control UI in your browser. If it loads, everything is working.
|
||||
|
||||
</Step>
|
||||
<Step title="Send your first message">
|
||||
Type a message in the Control UI chat and you should get an AI reply.
|
||||
|
||||
Want to chat from your phone instead? The fastest channel to set up is
|
||||
[Telegram](/channels/telegram) (just a bot token). See [Channels](/channels)
|
||||
for all options.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Check>
|
||||
If the Control UI loads, your Gateway is ready for use.
|
||||
</Check>
|
||||
|
||||
## Optional checks and extras
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Run the Gateway in the foreground">
|
||||
Useful for quick tests or troubleshooting.
|
||||
|
||||
```bash
|
||||
openclaw gateway --port 18789
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Send a test message">
|
||||
Requires a configured channel.
|
||||
|
||||
```bash
|
||||
openclaw message send --target +15555550123 --message "Hello from OpenClaw"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Useful environment variables
|
||||
|
||||
If you run OpenClaw as a service account or want custom config/state locations:
|
||||
|
||||
- `OPENCLAW_HOME` sets the home directory used for internal path resolution.
|
||||
- `OPENCLAW_STATE_DIR` overrides the state directory.
|
||||
- `OPENCLAW_CONFIG_PATH` overrides the config file path.
|
||||
|
||||
Full environment variable reference: [Environment vars](/help/environment).
|
||||
|
||||
## Go deeper
|
||||
## What to do next
|
||||
|
||||
<Columns>
|
||||
<Card title="Onboarding (CLI)" href="/start/wizard">
|
||||
Full CLI onboarding reference and advanced options.
|
||||
<Card title="Connect a channel" href="/channels" icon="message-square">
|
||||
WhatsApp, Telegram, Discord, iMessage, and more.
|
||||
</Card>
|
||||
<Card title="macOS app onboarding" href="/start/onboarding">
|
||||
First run flow for the macOS app.
|
||||
<Card title="Pairing and safety" href="/channels/pairing" icon="shield">
|
||||
Control who can message your agent.
|
||||
</Card>
|
||||
<Card title="Configure the Gateway" href="/gateway/configuration" icon="settings">
|
||||
Models, tools, sandbox, and advanced settings.
|
||||
</Card>
|
||||
<Card title="Browse tools" href="/tools" icon="wrench">
|
||||
Browser, exec, web search, skills, and plugins.
|
||||
</Card>
|
||||
</Columns>
|
||||
|
||||
## What you will have
|
||||
<Accordion title="Advanced: environment variables">
|
||||
If you run OpenClaw as a service account or want custom paths:
|
||||
|
||||
- A running Gateway
|
||||
- Auth configured
|
||||
- Control UI access or a connected channel
|
||||
- `OPENCLAW_HOME` — home directory for internal path resolution
|
||||
- `OPENCLAW_STATE_DIR` — override the state directory
|
||||
- `OPENCLAW_CONFIG_PATH` — override the config file path
|
||||
|
||||
## Next steps
|
||||
|
||||
- DM safety and approvals: [Pairing](/channels/pairing)
|
||||
- Connect more channels: [Channels](/channels)
|
||||
- Advanced workflows and from source: [Setup](/start/setup)
|
||||
Full reference: [Environment variables](/help/environment).
|
||||
</Accordion>
|
||||
|
||||
@ -17,7 +17,6 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
||||
|
||||
- [Index](/)
|
||||
- [Getting Started](/start/getting-started)
|
||||
- [Quick start](/start/quickstart)
|
||||
- [Onboarding](/start/onboarding)
|
||||
- [Onboarding (CLI)](/start/wizard)
|
||||
- [Setup](/start/setup)
|
||||
|
||||
@ -9,43 +9,59 @@ sidebarTitle: "Onboarding Overview"
|
||||
|
||||
# Onboarding Overview
|
||||
|
||||
OpenClaw supports multiple onboarding paths depending on where the Gateway runs
|
||||
and how you prefer to configure providers.
|
||||
OpenClaw has two onboarding paths. Both configure auth, the Gateway, and
|
||||
optional channels — they just differ in how you interact with the setup.
|
||||
|
||||
## Choose your onboarding path
|
||||
## Which path should I use?
|
||||
|
||||
- **CLI onboarding** for macOS, Linux, and Windows (via WSL2).
|
||||
- **macOS app** for a guided first run on Apple silicon or Intel Macs.
|
||||
| | CLI onboarding | macOS app onboarding |
|
||||
| -------------- | -------------------------------------- | ------------------------- |
|
||||
| **Platforms** | macOS, Linux, Windows (native or WSL2) | macOS only |
|
||||
| **Interface** | Terminal wizard | Guided UI in the app |
|
||||
| **Best for** | Servers, headless, full control | Desktop Mac, visual setup |
|
||||
| **Automation** | `--non-interactive` for scripts | Manual only |
|
||||
| **Command** | `openclaw onboard` | Launch the app |
|
||||
|
||||
Most users should start with **CLI onboarding** — it works everywhere and gives
|
||||
you the most control.
|
||||
|
||||
## What onboarding configures
|
||||
|
||||
Regardless of which path you choose, onboarding sets up:
|
||||
|
||||
1. **Model provider and auth** — API key, OAuth, or setup token for your chosen provider
|
||||
2. **Workspace** — directory for agent files, bootstrap templates, and memory
|
||||
3. **Gateway** — port, bind address, auth mode
|
||||
4. **Channels** (optional) — WhatsApp, Telegram, Discord, and more
|
||||
5. **Daemon** (optional) — background service so the Gateway starts automatically
|
||||
|
||||
## CLI onboarding
|
||||
|
||||
Run onboarding in a terminal:
|
||||
Run in any terminal:
|
||||
|
||||
```bash
|
||||
openclaw onboard
|
||||
```
|
||||
|
||||
Use CLI onboarding when you want full control of the Gateway, workspace,
|
||||
channels, and skills. Docs:
|
||||
Add `--install-daemon` to also install the background service in one step.
|
||||
|
||||
- [Onboarding (CLI)](/start/wizard)
|
||||
- [`openclaw onboard` command](/cli/onboard)
|
||||
Full reference: [Onboarding (CLI)](/start/wizard)
|
||||
CLI command docs: [`openclaw onboard`](/cli/onboard)
|
||||
|
||||
## macOS app onboarding
|
||||
|
||||
Use the OpenClaw app when you want a fully guided setup on macOS. Docs:
|
||||
Open the OpenClaw app. The first-run wizard walks you through the same steps
|
||||
with a visual interface.
|
||||
|
||||
- [Onboarding (macOS App)](/start/onboarding)
|
||||
Full reference: [Onboarding (macOS App)](/start/onboarding)
|
||||
|
||||
## Custom Provider
|
||||
## Custom or unlisted providers
|
||||
|
||||
If you need an endpoint that is not listed, including hosted providers that
|
||||
expose standard OpenAI or Anthropic APIs, choose **Custom Provider** in the
|
||||
CLI onboarding. You will be asked to:
|
||||
If your provider is not listed in onboarding, choose **Custom Provider** and
|
||||
enter:
|
||||
|
||||
- Pick OpenAI-compatible, Anthropic-compatible, or **Unknown** (auto-detect).
|
||||
- Enter a base URL and API key (if required by the provider).
|
||||
- Provide a model ID and optional alias.
|
||||
- Choose an Endpoint ID so multiple custom endpoints can coexist.
|
||||
- API compatibility mode (OpenAI-compatible, Anthropic-compatible, or auto-detect)
|
||||
- Base URL and API key
|
||||
- Model ID and optional alias
|
||||
|
||||
For detailed steps, follow the CLI onboarding docs above.
|
||||
Multiple custom endpoints can coexist — each gets its own endpoint ID.
|
||||
|
||||
@ -8,13 +8,13 @@ title: "Personal Assistant Setup"
|
||||
|
||||
# Building a personal assistant with OpenClaw
|
||||
|
||||
OpenClaw is a WhatsApp + Telegram + Discord + iMessage gateway for **Pi** agents. Plugins add Mattermost. This guide is the "personal assistant" setup: one dedicated WhatsApp number that behaves like your always-on agent.
|
||||
OpenClaw is a self-hosted gateway that connects WhatsApp, Telegram, Discord, iMessage, and more to AI agents. This guide covers the "personal assistant" setup: a dedicated WhatsApp number that behaves like your always-on AI assistant.
|
||||
|
||||
## ⚠️ Safety first
|
||||
|
||||
You’re putting an agent in a position to:
|
||||
|
||||
- run commands on your machine (depending on your Pi tool setup)
|
||||
- run commands on your machine (depending on your tool policy)
|
||||
- read/write files in your workspace
|
||||
- send messages back out via WhatsApp/Telegram/Discord/Mattermost (plugin)
|
||||
|
||||
@ -36,7 +36,7 @@ You want this:
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A["<b>Your Phone (personal)<br></b><br>Your WhatsApp<br>+1-555-YOU"] -- message --> B["<b>Second Phone (assistant)<br></b><br>Assistant WA<br>+1-555-ASSIST"]
|
||||
B -- linked via QR --> C["<b>Your Mac (openclaw)<br></b><br>Pi agent"]
|
||||
B -- linked via QR --> C["<b>Your Mac (openclaw)<br></b><br>AI agent"]
|
||||
```
|
||||
|
||||
If you link your personal WhatsApp to OpenClaw, every message to you becomes “agent input”. That’s rarely what you want.
|
||||
|
||||
@ -13,8 +13,6 @@ If you are setting up for the first time, start with [Getting Started](/start/ge
|
||||
For onboarding details, see [Onboarding (CLI)](/start/wizard).
|
||||
</Note>
|
||||
|
||||
Last updated: 2026-01-01
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Tailoring lives outside the repo:** `~/.openclaw/workspace` (workspace) + `~/.openclaw/openclaw.json` (config).
|
||||
@ -23,7 +21,7 @@ Last updated: 2026-01-01
|
||||
|
||||
## Prereqs (from source)
|
||||
|
||||
- Node `>=22`
|
||||
- Node 24 recommended (Node 22 LTS, currently `22.16+`, still supported)
|
||||
- `pnpm`
|
||||
- Docker (optional; only for containerized setup/e2e — see [Docker](/install/docker))
|
||||
|
||||
|
||||
@ -415,7 +415,7 @@ Some controls depend on backend capabilities. If a backend does not support a co
|
||||
| `/acp cwd` | Set runtime working directory override. | `/acp cwd /Users/user/Projects/repo` |
|
||||
| `/acp permissions` | Set approval policy profile. | `/acp permissions strict` |
|
||||
| `/acp timeout` | Set runtime timeout (seconds). | `/acp timeout 120` |
|
||||
| `/acp model` | Set runtime model override. | `/acp model anthropic/claude-opus-4-5` |
|
||||
| `/acp model` | Set runtime model override. | `/acp model anthropic/claude-opus-4-6` |
|
||||
| `/acp reset-options` | Remove session runtime option overrides. | `/acp reset-options` |
|
||||
| `/acp sessions` | List recent ACP sessions from store. | `/acp sessions` |
|
||||
| `/acp doctor` | Backend health, capabilities, actionable fixes. | `/acp doctor` |
|
||||
|
||||
93
docs/tools/brave-search.md
Normal file
93
docs/tools/brave-search.md
Normal file
@ -0,0 +1,93 @@
|
||||
---
|
||||
summary: "Brave Search API setup for web_search"
|
||||
read_when:
|
||||
- You want to use Brave Search for web_search
|
||||
- You need a BRAVE_API_KEY or plan details
|
||||
title: "Brave Search"
|
||||
---
|
||||
|
||||
# Brave Search API
|
||||
|
||||
OpenClaw supports Brave Search API as a `web_search` provider.
|
||||
|
||||
## Get an API key
|
||||
|
||||
1. Create a Brave Search API account at [https://brave.com/search/api/](https://brave.com/search/api/)
|
||||
2. In the dashboard, choose the **Search** plan and generate an API key.
|
||||
3. Store the key in config or set `BRAVE_API_KEY` in the Gateway environment.
|
||||
|
||||
## Config example
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
brave: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "BRAVE_API_KEY_HERE",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brave",
|
||||
maxResults: 5,
|
||||
timeoutSeconds: 30,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Provider-specific Brave search settings now live under `plugins.entries.brave.config.webSearch.*`.
|
||||
Legacy `tools.web.search.apiKey` still loads through the compatibility shim, but it is no longer the canonical config path.
|
||||
|
||||
## Tool parameters
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------- | ------------------------------------------------------------------- |
|
||||
| `query` | Search query (required) |
|
||||
| `count` | Number of results to return (1-10, default: 5) |
|
||||
| `country` | 2-letter ISO country code (e.g., "US", "DE") |
|
||||
| `language` | ISO 639-1 language code for search results (e.g., "en", "de", "fr") |
|
||||
| `ui_lang` | ISO language code for UI elements |
|
||||
| `freshness` | Time filter: `day` (24h), `week`, `month`, or `year` |
|
||||
| `date_after` | Only results published after this date (YYYY-MM-DD) |
|
||||
| `date_before` | Only results published before this date (YYYY-MM-DD) |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```javascript
|
||||
// Country and language-specific search
|
||||
await web_search({
|
||||
query: "renewable energy",
|
||||
country: "DE",
|
||||
language: "de",
|
||||
});
|
||||
|
||||
// Recent results (past week)
|
||||
await web_search({
|
||||
query: "AI news",
|
||||
freshness: "week",
|
||||
});
|
||||
|
||||
// Date range search
|
||||
await web_search({
|
||||
query: "AI developments",
|
||||
date_after: "2024-01-01",
|
||||
date_before: "2024-06-30",
|
||||
});
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- OpenClaw uses the Brave **Search** plan. If you have a legacy subscription (e.g. the original Free plan with 2,000 queries/month), it remains valid but does not include newer features like LLM Context or higher rate limits.
|
||||
- Each Brave plan includes **\$5/month in free credit** (renewing). The Search plan costs \$5 per 1,000 requests, so the credit covers 1,000 queries/month. Set your usage limit in the Brave dashboard to avoid unexpected charges. See the [Brave API portal](https://brave.com/search/api/) for current plans.
|
||||
- The Search plan includes the LLM Context endpoint and AI inference rights. Storing results to train or tune models requires a plan with explicit storage rights. See the Brave [Terms of Service](https://api-dashboard.search.brave.com/terms-of-service).
|
||||
- Results are cached for 15 minutes by default (configurable via `cacheTtlMinutes`).
|
||||
|
||||
See [Web tools](/tools/web) for the full web_search configuration.
|
||||
@ -581,7 +581,7 @@ Notes:
|
||||
- `--format ai` (default when Playwright is installed): returns an AI snapshot with numeric refs (`aria-ref="<n>"`).
|
||||
- `--format aria`: returns the accessibility tree (no refs; inspection only).
|
||||
- `--efficient` (or `--mode efficient`): compact role snapshot preset (interactive + compact + depth + lower maxChars).
|
||||
- Config default (tool/CLI only): set `browser.snapshotDefaults.mode: "efficient"` to use efficient snapshots when the caller does not pass a mode (see [Gateway configuration](/gateway/configuration#browser-openclaw-managed-browser)).
|
||||
- Config default (tool/CLI only): set `browser.snapshotDefaults.mode: "efficient"` to use efficient snapshots when the caller does not pass a mode (see [Gateway configuration](/gateway/configuration-reference#browser)).
|
||||
- Role snapshot options (`--interactive`, `--compact`, `--depth`, `--selector`) force a role-based snapshot with refs like `ref=e12`.
|
||||
- `--frame "<iframe selector>"` scopes role snapshots to an iframe (pairs with role refs like `e12`).
|
||||
- `--interactive` outputs a flat, easy-to-pick list of interactive elements (best for driving actions).
|
||||
|
||||
@ -360,5 +360,5 @@ After configuring multi-agent sandbox and tools:
|
||||
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) -- debugging "why is this blocked?"
|
||||
- [Elevated Mode](/tools/elevated)
|
||||
- [Multi-Agent Routing](/concepts/multi-agent)
|
||||
- [Sandbox Configuration](/gateway/configuration#agentsdefaults-sandbox)
|
||||
- [Sandbox Configuration](/gateway/configuration-reference#agents-defaults-sandbox)
|
||||
- [Session Management](/concepts/session)
|
||||
|
||||
174
docs/tools/perplexity-search.md
Normal file
174
docs/tools/perplexity-search.md
Normal file
@ -0,0 +1,174 @@
|
||||
---
|
||||
summary: "Perplexity Search API and Sonar/OpenRouter compatibility for web_search"
|
||||
read_when:
|
||||
- You want to use Perplexity Search for web search
|
||||
- You need PERPLEXITY_API_KEY or OPENROUTER_API_KEY setup
|
||||
title: "Perplexity Search"
|
||||
---
|
||||
|
||||
# Perplexity Search API
|
||||
|
||||
OpenClaw supports Perplexity Search API as a `web_search` provider.
|
||||
It returns structured results with `title`, `url`, and `snippet` fields.
|
||||
|
||||
For compatibility, OpenClaw also supports legacy Perplexity Sonar/OpenRouter setups.
|
||||
If you use `OPENROUTER_API_KEY`, an `sk-or-...` key in `plugins.entries.perplexity.config.webSearch.apiKey`, or set `plugins.entries.perplexity.config.webSearch.baseUrl` / `model`, the provider switches to the chat-completions path and returns AI-synthesized answers with citations instead of structured Search API results.
|
||||
|
||||
## Getting a Perplexity API key
|
||||
|
||||
1. Create a Perplexity account at [perplexity.ai/settings/api](https://www.perplexity.ai/settings/api)
|
||||
2. Generate an API key in the dashboard
|
||||
3. Store the key in config or set `PERPLEXITY_API_KEY` in the Gateway environment.
|
||||
|
||||
## OpenRouter compatibility
|
||||
|
||||
If you were already using OpenRouter for Perplexity Sonar, keep `provider: "perplexity"` and set `OPENROUTER_API_KEY` in the Gateway environment, or store an `sk-or-...` key in `plugins.entries.perplexity.config.webSearch.apiKey`.
|
||||
|
||||
Optional compatibility controls:
|
||||
|
||||
- `plugins.entries.perplexity.config.webSearch.baseUrl`
|
||||
- `plugins.entries.perplexity.config.webSearch.model`
|
||||
|
||||
## Config examples
|
||||
|
||||
### Native Perplexity Search API
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
perplexity: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "pplx-...",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "perplexity",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### OpenRouter / Sonar compatibility
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
perplexity: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "<openrouter-api-key>",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
model: "perplexity/sonar-pro",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "perplexity",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Where to set the key
|
||||
|
||||
**Via config:** run `openclaw configure --section web`. It stores the key in
|
||||
`~/.openclaw/openclaw.json` under `plugins.entries.perplexity.config.webSearch.apiKey`.
|
||||
That field also accepts SecretRef objects.
|
||||
|
||||
**Via environment:** set `PERPLEXITY_API_KEY` or `OPENROUTER_API_KEY`
|
||||
in the Gateway process environment. For a gateway install, put it in
|
||||
`~/.openclaw/.env` (or your service environment). See [Env vars](/help/faq#how-does-openclaw-load-environment-variables).
|
||||
|
||||
If `provider: "perplexity"` is configured and the Perplexity key SecretRef is unresolved with no env fallback, startup/reload fails fast.
|
||||
|
||||
## Tool parameters
|
||||
|
||||
These parameters apply to the native Perplexity Search API path.
|
||||
|
||||
| Parameter | Description |
|
||||
| --------------------- | ---------------------------------------------------- |
|
||||
| `query` | Search query (required) |
|
||||
| `count` | Number of results to return (1-10, default: 5) |
|
||||
| `country` | 2-letter ISO country code (e.g., "US", "DE") |
|
||||
| `language` | ISO 639-1 language code (e.g., "en", "de", "fr") |
|
||||
| `freshness` | Time filter: `day` (24h), `week`, `month`, or `year` |
|
||||
| `date_after` | Only results published after this date (YYYY-MM-DD) |
|
||||
| `date_before` | Only results published before this date (YYYY-MM-DD) |
|
||||
| `domain_filter` | Domain allowlist/denylist array (max 20) |
|
||||
| `max_tokens` | Total content budget (default: 25000, max: 1000000) |
|
||||
| `max_tokens_per_page` | Per-page token limit (default: 2048) |
|
||||
|
||||
For the legacy Sonar/OpenRouter compatibility path, only `query` and `freshness` are supported.
|
||||
Search API-only filters such as `country`, `language`, `date_after`, `date_before`, `domain_filter`, `max_tokens`, and `max_tokens_per_page` return explicit errors.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```javascript
|
||||
// Country and language-specific search
|
||||
await web_search({
|
||||
query: "renewable energy",
|
||||
country: "DE",
|
||||
language: "de",
|
||||
});
|
||||
|
||||
// Recent results (past week)
|
||||
await web_search({
|
||||
query: "AI news",
|
||||
freshness: "week",
|
||||
});
|
||||
|
||||
// Date range search
|
||||
await web_search({
|
||||
query: "AI developments",
|
||||
date_after: "2024-01-01",
|
||||
date_before: "2024-06-30",
|
||||
});
|
||||
|
||||
// Domain filtering (allowlist)
|
||||
await web_search({
|
||||
query: "climate research",
|
||||
domain_filter: ["nature.com", "science.org", ".edu"],
|
||||
});
|
||||
|
||||
// Domain filtering (denylist - prefix with -)
|
||||
await web_search({
|
||||
query: "product reviews",
|
||||
domain_filter: ["-reddit.com", "-pinterest.com"],
|
||||
});
|
||||
|
||||
// More content extraction
|
||||
await web_search({
|
||||
query: "detailed AI research",
|
||||
max_tokens: 50000,
|
||||
max_tokens_per_page: 4096,
|
||||
});
|
||||
```
|
||||
|
||||
### Domain filter rules
|
||||
|
||||
- Maximum 20 domains per filter
|
||||
- Cannot mix allowlist and denylist in the same request
|
||||
- Use `-` prefix for denylist entries (e.g., `["-reddit.com"]`)
|
||||
|
||||
## Notes
|
||||
|
||||
- Perplexity Search API returns structured web search results (`title`, `url`, `snippet`)
|
||||
- OpenRouter or explicit `plugins.entries.perplexity.config.webSearch.baseUrl` / `model` switches Perplexity back to Sonar chat completions for compatibility
|
||||
- Results are cached for 15 minutes by default (configurable via `cacheTtlMinutes`)
|
||||
|
||||
See [Web tools](/tools/web) for the full web_search configuration.
|
||||
See [Perplexity Search API docs](https://docs.perplexity.ai/docs/search/quickstart) for more details.
|
||||
@ -286,7 +286,7 @@ openclaw plugins install ./plugin.zip # install from a local zip
|
||||
openclaw plugins install -l ./extensions/voice-call # link (no copy) for dev
|
||||
openclaw plugins install @openclaw/voice-call # install from npm
|
||||
openclaw plugins install @openclaw/voice-call --pin # store exact resolved name@version
|
||||
openclaw plugins update <id>
|
||||
openclaw plugins update <id-or-npm-spec>
|
||||
openclaw plugins update --all
|
||||
openclaw plugins enable <id>
|
||||
openclaw plugins disable <id>
|
||||
|
||||
@ -98,7 +98,7 @@ Text + native (when enabled):
|
||||
- `/plugins list|show|get|enable|disable` (inspect discovered plugins and toggle enablement, owner-only for writes; requires `commands.plugins: true`)
|
||||
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
|
||||
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
|
||||
- `/tts off|always|inbound|tagged|status|provider|limit|summary|audio` (control TTS; see [/tts](/tts))
|
||||
- `/tts off|always|inbound|tagged|status|provider|limit|summary|audio` (control TTS; see [/tts](/tools/tts))
|
||||
- Discord: native command is `/voice` (Discord reserves `/tts`); text `/tts` still works.
|
||||
- `/stop`
|
||||
- `/restart`
|
||||
|
||||
406
docs/tools/tts.md
Normal file
406
docs/tools/tts.md
Normal file
@ -0,0 +1,406 @@
|
||||
---
|
||||
summary: "Text-to-speech (TTS) for outbound replies"
|
||||
read_when:
|
||||
- Enabling text-to-speech for replies
|
||||
- Configuring TTS providers or limits
|
||||
- Using /tts commands
|
||||
title: "Text-to-Speech"
|
||||
---
|
||||
|
||||
# Text-to-speech (TTS)
|
||||
|
||||
OpenClaw can convert outbound replies into audio using ElevenLabs, Microsoft, or OpenAI.
|
||||
It works anywhere OpenClaw can send audio; Telegram gets a round voice-note bubble.
|
||||
|
||||
## Supported services
|
||||
|
||||
- **ElevenLabs** (primary or fallback provider)
|
||||
- **Microsoft** (primary or fallback provider; current bundled implementation uses `node-edge-tts`, default when no API keys)
|
||||
- **OpenAI** (primary or fallback provider; also used for summaries)
|
||||
|
||||
### Microsoft speech notes
|
||||
|
||||
The bundled Microsoft speech provider currently uses Microsoft Edge's online
|
||||
neural TTS service via the `node-edge-tts` library. It's a hosted service (not
|
||||
local), uses Microsoft endpoints, and does not require an API key.
|
||||
`node-edge-tts` exposes speech configuration options and output formats, but
|
||||
not all options are supported by the service. Legacy config and directive input
|
||||
using `edge` still works and is normalized to `microsoft`.
|
||||
|
||||
Because this path is a public web service without a published SLA or quota,
|
||||
treat it as best-effort. If you need guaranteed limits and support, use OpenAI
|
||||
or ElevenLabs.
|
||||
|
||||
## Optional keys
|
||||
|
||||
If you want OpenAI or ElevenLabs:
|
||||
|
||||
- `ELEVENLABS_API_KEY` (or `XI_API_KEY`)
|
||||
- `OPENAI_API_KEY`
|
||||
|
||||
Microsoft speech does **not** require an API key. If no API keys are found,
|
||||
OpenClaw defaults to Microsoft (unless disabled via
|
||||
`messages.tts.microsoft.enabled=false` or `messages.tts.edge.enabled=false`).
|
||||
|
||||
If multiple providers are configured, the selected provider is used first and the others are fallback options.
|
||||
Auto-summary uses the configured `summaryModel` (or `agents.defaults.model.primary`),
|
||||
so that provider must also be authenticated if you enable summaries.
|
||||
|
||||
## Service links
|
||||
|
||||
- [OpenAI Text-to-Speech guide](https://platform.openai.com/docs/guides/text-to-speech)
|
||||
- [OpenAI Audio API reference](https://platform.openai.com/docs/api-reference/audio)
|
||||
- [ElevenLabs Text to Speech](https://elevenlabs.io/docs/api-reference/text-to-speech)
|
||||
- [ElevenLabs Authentication](https://elevenlabs.io/docs/api-reference/authentication)
|
||||
- [node-edge-tts](https://github.com/SchneeHertz/node-edge-tts)
|
||||
- [Microsoft Speech output formats](https://learn.microsoft.com/azure/ai-services/speech-service/rest-text-to-speech#audio-outputs)
|
||||
|
||||
## Is it enabled by default?
|
||||
|
||||
No. Auto‑TTS is **off** by default. Enable it in config with
|
||||
`messages.tts.auto` or per session with `/tts always` (alias: `/tts on`).
|
||||
|
||||
Microsoft speech **is** enabled by default once TTS is on, and is used automatically
|
||||
when no OpenAI or ElevenLabs API keys are available.
|
||||
|
||||
## Config
|
||||
|
||||
TTS config lives under `messages.tts` in `openclaw.json`.
|
||||
Full schema is in [Gateway configuration](/gateway/configuration).
|
||||
|
||||
### Minimal config (enable + provider)
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
auto: "always",
|
||||
provider: "elevenlabs",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAI primary with ElevenLabs fallback
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
auto: "always",
|
||||
provider: "openai",
|
||||
summaryModel: "openai/gpt-4.1-mini",
|
||||
modelOverrides: {
|
||||
enabled: true,
|
||||
},
|
||||
openai: {
|
||||
apiKey: "openai_api_key",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
model: "gpt-4o-mini-tts",
|
||||
voice: "alloy",
|
||||
},
|
||||
elevenlabs: {
|
||||
apiKey: "elevenlabs_api_key",
|
||||
baseUrl: "https://api.elevenlabs.io",
|
||||
voiceId: "voice_id",
|
||||
modelId: "eleven_multilingual_v2",
|
||||
seed: 42,
|
||||
applyTextNormalization: "auto",
|
||||
languageCode: "en",
|
||||
voiceSettings: {
|
||||
stability: 0.5,
|
||||
similarityBoost: 0.75,
|
||||
style: 0.0,
|
||||
useSpeakerBoost: true,
|
||||
speed: 1.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Microsoft primary (no API key)
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
auto: "always",
|
||||
provider: "microsoft",
|
||||
microsoft: {
|
||||
enabled: true,
|
||||
voice: "en-US-MichelleNeural",
|
||||
lang: "en-US",
|
||||
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
|
||||
rate: "+10%",
|
||||
pitch: "-5%",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Disable Microsoft speech
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
microsoft: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Custom limits + prefs path
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
auto: "always",
|
||||
maxTextLength: 4000,
|
||||
timeoutMs: 30000,
|
||||
prefsPath: "~/.openclaw/settings/tts.json",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Only reply with audio after an inbound voice note
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
auto: "inbound",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Disable auto-summary for long replies
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
auto: "always",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```
|
||||
/tts summary off
|
||||
```
|
||||
|
||||
### Notes on fields
|
||||
|
||||
- `auto`: auto‑TTS mode (`off`, `always`, `inbound`, `tagged`).
|
||||
- `inbound` only sends audio after an inbound voice note.
|
||||
- `tagged` only sends audio when the reply includes `[[tts]]` tags.
|
||||
- `enabled`: legacy toggle (doctor migrates this to `auto`).
|
||||
- `mode`: `"final"` (default) or `"all"` (includes tool/block replies).
|
||||
- `provider`: speech provider id such as `"elevenlabs"`, `"microsoft"`, or `"openai"` (fallback is automatic).
|
||||
- If `provider` is **unset**, OpenClaw prefers `openai` (if key), then `elevenlabs` (if key),
|
||||
otherwise `microsoft`.
|
||||
- Legacy `provider: "edge"` still works and is normalized to `microsoft`.
|
||||
- `summaryModel`: optional cheap model for auto-summary; defaults to `agents.defaults.model.primary`.
|
||||
- Accepts `provider/model` or a configured model alias.
|
||||
- `modelOverrides`: allow the model to emit TTS directives (on by default).
|
||||
- `allowProvider` defaults to `false` (provider switching is opt-in).
|
||||
- `maxTextLength`: hard cap for TTS input (chars). `/tts audio` fails if exceeded.
|
||||
- `timeoutMs`: request timeout (ms).
|
||||
- `prefsPath`: override the local prefs JSON path (provider/limit/summary).
|
||||
- `apiKey` values fall back to env vars (`ELEVENLABS_API_KEY`/`XI_API_KEY`, `OPENAI_API_KEY`).
|
||||
- `elevenlabs.baseUrl`: override ElevenLabs API base URL.
|
||||
- `openai.baseUrl`: override the OpenAI TTS endpoint.
|
||||
- Resolution order: `messages.tts.openai.baseUrl` -> `OPENAI_TTS_BASE_URL` -> `https://api.openai.com/v1`
|
||||
- Non-default values are treated as OpenAI-compatible TTS endpoints, so custom model and voice names are accepted.
|
||||
- `elevenlabs.voiceSettings`:
|
||||
- `stability`, `similarityBoost`, `style`: `0..1`
|
||||
- `useSpeakerBoost`: `true|false`
|
||||
- `speed`: `0.5..2.0` (1.0 = normal)
|
||||
- `elevenlabs.applyTextNormalization`: `auto|on|off`
|
||||
- `elevenlabs.languageCode`: 2-letter ISO 639-1 (e.g. `en`, `de`)
|
||||
- `elevenlabs.seed`: integer `0..4294967295` (best-effort determinism)
|
||||
- `microsoft.enabled`: allow Microsoft speech usage (default `true`; no API key).
|
||||
- `microsoft.voice`: Microsoft neural voice name (e.g. `en-US-MichelleNeural`).
|
||||
- `microsoft.lang`: language code (e.g. `en-US`).
|
||||
- `microsoft.outputFormat`: Microsoft output format (e.g. `audio-24khz-48kbitrate-mono-mp3`).
|
||||
- See Microsoft Speech output formats for valid values; not all formats are supported by the bundled Edge-backed transport.
|
||||
- `microsoft.rate` / `microsoft.pitch` / `microsoft.volume`: percent strings (e.g. `+10%`, `-5%`).
|
||||
- `microsoft.saveSubtitles`: write JSON subtitles alongside the audio file.
|
||||
- `microsoft.proxy`: proxy URL for Microsoft speech requests.
|
||||
- `microsoft.timeoutMs`: request timeout override (ms).
|
||||
- `edge.*`: legacy alias for the same Microsoft settings.
|
||||
|
||||
## Model-driven overrides (default on)
|
||||
|
||||
By default, the model **can** emit TTS directives for a single reply.
|
||||
When `messages.tts.auto` is `tagged`, these directives are required to trigger audio.
|
||||
|
||||
When enabled, the model can emit `[[tts:...]]` directives to override the voice
|
||||
for a single reply, plus an optional `[[tts:text]]...[[/tts:text]]` block to
|
||||
provide expressive tags (laughter, singing cues, etc) that should only appear in
|
||||
the audio.
|
||||
|
||||
`provider=...` directives are ignored unless `modelOverrides.allowProvider: true`.
|
||||
|
||||
Example reply payload:
|
||||
|
||||
```
|
||||
Here you go.
|
||||
|
||||
[[tts:voiceId=pMsXgVXv3BLzUgSXRplE model=eleven_v3 speed=1.1]]
|
||||
[[tts:text]](laughs) Read the song once more.[[/tts:text]]
|
||||
```
|
||||
|
||||
Available directive keys (when enabled):
|
||||
|
||||
- `provider` (registered speech provider id, for example `openai`, `elevenlabs`, or `microsoft`; requires `allowProvider: true`)
|
||||
- `voice` (OpenAI voice) or `voiceId` (ElevenLabs)
|
||||
- `model` (OpenAI TTS model or ElevenLabs model id)
|
||||
- `stability`, `similarityBoost`, `style`, `speed`, `useSpeakerBoost`
|
||||
- `applyTextNormalization` (`auto|on|off`)
|
||||
- `languageCode` (ISO 639-1)
|
||||
- `seed`
|
||||
|
||||
Disable all model overrides:
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
modelOverrides: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Optional allowlist (enable provider switching while keeping other knobs configurable):
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
modelOverrides: {
|
||||
enabled: true,
|
||||
allowProvider: true,
|
||||
allowSeed: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Per-user preferences
|
||||
|
||||
Slash commands write local overrides to `prefsPath` (default:
|
||||
`~/.openclaw/settings/tts.json`, override with `OPENCLAW_TTS_PREFS` or
|
||||
`messages.tts.prefsPath`).
|
||||
|
||||
Stored fields:
|
||||
|
||||
- `enabled`
|
||||
- `provider`
|
||||
- `maxLength` (summary threshold; default 1500 chars)
|
||||
- `summarize` (default `true`)
|
||||
|
||||
These override `messages.tts.*` for that host.
|
||||
|
||||
## Output formats (fixed)
|
||||
|
||||
- **Telegram**: Opus voice note (`opus_48000_64` from ElevenLabs, `opus` from OpenAI).
|
||||
- 48kHz / 64kbps is a good voice-note tradeoff and required for the round bubble.
|
||||
- **Other channels**: MP3 (`mp3_44100_128` from ElevenLabs, `mp3` from OpenAI).
|
||||
- 44.1kHz / 128kbps is the default balance for speech clarity.
|
||||
- **Microsoft**: uses `microsoft.outputFormat` (default `audio-24khz-48kbitrate-mono-mp3`).
|
||||
- The bundled transport accepts an `outputFormat`, but not all formats are available from the service.
|
||||
- Output format values follow Microsoft Speech output formats (including Ogg/WebM Opus).
|
||||
- Telegram `sendVoice` accepts OGG/MP3/M4A; use OpenAI/ElevenLabs if you need
|
||||
guaranteed Opus voice notes. citeturn1search1
|
||||
- If the configured Microsoft output format fails, OpenClaw retries with MP3.
|
||||
|
||||
OpenAI/ElevenLabs formats are fixed; Telegram expects Opus for voice-note UX.
|
||||
|
||||
## Auto-TTS behavior
|
||||
|
||||
When enabled, OpenClaw:
|
||||
|
||||
- skips TTS if the reply already contains media or a `MEDIA:` directive.
|
||||
- skips very short replies (< 10 chars).
|
||||
- summarizes long replies when enabled using `agents.defaults.model.primary` (or `summaryModel`).
|
||||
- attaches the generated audio to the reply.
|
||||
|
||||
If the reply exceeds `maxLength` and summary is off (or no API key for the
|
||||
summary model), audio
|
||||
is skipped and the normal text reply is sent.
|
||||
|
||||
## Flow diagram
|
||||
|
||||
```
|
||||
Reply -> TTS enabled?
|
||||
no -> send text
|
||||
yes -> has media / MEDIA: / short?
|
||||
yes -> send text
|
||||
no -> length > limit?
|
||||
no -> TTS -> attach audio
|
||||
yes -> summary enabled?
|
||||
no -> send text
|
||||
yes -> summarize (summaryModel or agents.defaults.model.primary)
|
||||
-> TTS -> attach audio
|
||||
```
|
||||
|
||||
## Slash command usage
|
||||
|
||||
There is a single command: `/tts`.
|
||||
See [Slash commands](/tools/slash-commands) for enablement details.
|
||||
|
||||
Discord note: `/tts` is a built-in Discord command, so OpenClaw registers
|
||||
`/voice` as the native command there. Text `/tts ...` still works.
|
||||
|
||||
```
|
||||
/tts off
|
||||
/tts always
|
||||
/tts inbound
|
||||
/tts tagged
|
||||
/tts status
|
||||
/tts provider openai
|
||||
/tts limit 2000
|
||||
/tts summary off
|
||||
/tts audio Hello from OpenClaw
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Commands require an authorized sender (allowlist/owner rules still apply).
|
||||
- `commands.text` or native command registration must be enabled.
|
||||
- `off|always|inbound|tagged` are per‑session toggles (`/tts on` is an alias for `/tts always`).
|
||||
- `limit` and `summary` are stored in local prefs, not the main config.
|
||||
- `/tts audio` generates a one-off audio reply (does not toggle TTS on).
|
||||
|
||||
## Agent tool
|
||||
|
||||
The `tts` tool converts text to speech and returns a `MEDIA:` path. When the
|
||||
result is Telegram-compatible, the tool includes `[[audio_as_voice]]` so
|
||||
Telegram sends a voice bubble.
|
||||
|
||||
## Gateway RPC
|
||||
|
||||
Gateway methods:
|
||||
|
||||
- `tts.status`
|
||||
- `tts.enable`
|
||||
- `tts.disable`
|
||||
- `tts.convert`
|
||||
- `tts.setProvider`
|
||||
- `tts.providers`
|
||||
@ -26,7 +26,7 @@ These are **not** browser automation. For JS-heavy sites or logins, use the
|
||||
- `web_fetch` is enabled by default (unless explicitly disabled).
|
||||
- The bundled Firecrawl plugin also adds `firecrawl_search` and `firecrawl_scrape` when enabled.
|
||||
|
||||
See [Brave Search setup](/brave-search) and [Perplexity Search setup](/perplexity) for provider-specific details.
|
||||
See [Brave Search setup](/tools/brave-search) and [Perplexity Search setup](/tools/perplexity-search) for provider-specific details.
|
||||
|
||||
## Choosing a search provider
|
||||
|
||||
|
||||
65
docs/vps.md
65
docs/vps.md
@ -1,49 +1,58 @@
|
||||
---
|
||||
summary: "VPS hosting hub for OpenClaw (Oracle/Fly/Hetzner/GCP/Azure/exe.dev)"
|
||||
summary: "Run OpenClaw on a Linux server or cloud VPS — provider picker, architecture, and tuning"
|
||||
read_when:
|
||||
- You want to run the Gateway in the cloud
|
||||
- You need a quick map of VPS/hosting guides
|
||||
title: "VPS Hosting"
|
||||
- You want to run the Gateway on a Linux server or cloud VPS
|
||||
- You need a quick map of hosting guides
|
||||
- You want generic Linux server tuning for OpenClaw
|
||||
title: "Linux Server"
|
||||
sidebarTitle: "Linux Server"
|
||||
---
|
||||
|
||||
# VPS hosting
|
||||
# Linux Server
|
||||
|
||||
This hub links to the supported VPS/hosting guides and explains how cloud
|
||||
deployments work at a high level.
|
||||
Run the OpenClaw Gateway on any Linux server or cloud VPS. This page helps you
|
||||
pick a provider, explains how cloud deployments work, and covers generic Linux
|
||||
tuning that applies everywhere.
|
||||
|
||||
## Pick a provider
|
||||
|
||||
- **Railway** (one‑click + browser setup): [Railway](/install/railway)
|
||||
- **Northflank** (one‑click + browser setup): [Northflank](/install/northflank)
|
||||
- **Oracle Cloud (Always Free)**: [Oracle](/platforms/oracle) — $0/month (Always Free, ARM; capacity/signup can be finicky)
|
||||
- **Fly.io**: [Fly.io](/install/fly)
|
||||
- **Hetzner (Docker)**: [Hetzner](/install/hetzner)
|
||||
- **GCP (Compute Engine)**: [GCP](/install/gcp)
|
||||
- **Azure (Linux VM)**: [Azure](/install/azure)
|
||||
- **exe.dev** (VM + HTTPS proxy): [exe.dev](/install/exe-dev)
|
||||
- **AWS (EC2/Lightsail/free tier)**: works well too. Video guide:
|
||||
[https://x.com/techfrenAJ/status/2014934471095812547](https://x.com/techfrenAJ/status/2014934471095812547)
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Railway" href="/install/railway">One-click, browser setup</Card>
|
||||
<Card title="Northflank" href="/install/northflank">One-click, browser setup</Card>
|
||||
<Card title="DigitalOcean" href="/install/digitalocean">Simple paid VPS</Card>
|
||||
<Card title="Oracle Cloud" href="/install/oracle">Always Free ARM tier</Card>
|
||||
<Card title="Fly.io" href="/install/fly">Fly Machines</Card>
|
||||
<Card title="Hetzner" href="/install/hetzner">Docker on Hetzner VPS</Card>
|
||||
<Card title="GCP" href="/install/gcp">Compute Engine</Card>
|
||||
<Card title="Azure" href="/install/azure">Linux VM</Card>
|
||||
<Card title="exe.dev" href="/install/exe-dev">VM with HTTPS proxy</Card>
|
||||
<Card title="Raspberry Pi" href="/install/raspberry-pi">ARM self-hosted</Card>
|
||||
</CardGroup>
|
||||
|
||||
**AWS (EC2 / Lightsail / free tier)** also works well.
|
||||
A community video walkthrough is available at
|
||||
[x.com/techfrenAJ/status/2014934471095812547](https://x.com/techfrenAJ/status/2014934471095812547)
|
||||
(community resource -- may become unavailable).
|
||||
|
||||
## How cloud setups work
|
||||
|
||||
- The **Gateway runs on the VPS** and owns state + workspace.
|
||||
- You connect from your laptop/phone via the **Control UI** or **Tailscale/SSH**.
|
||||
- Treat the VPS as the source of truth and **back up** the state + workspace.
|
||||
- You connect from your laptop or phone via the **Control UI** or **Tailscale/SSH**.
|
||||
- Treat the VPS as the source of truth and **back up** the state + workspace regularly.
|
||||
- Secure default: keep the Gateway on loopback and access it via SSH tunnel or Tailscale Serve.
|
||||
If you bind to `lan`/`tailnet`, require `gateway.auth.token` or `gateway.auth.password`.
|
||||
If you bind to `lan` or `tailnet`, require `gateway.auth.token` or `gateway.auth.password`.
|
||||
|
||||
Remote access: [Gateway remote](/gateway/remote)
|
||||
Platforms hub: [Platforms](/platforms)
|
||||
Related pages: [Gateway remote access](/gateway/remote), [Platforms hub](/platforms).
|
||||
|
||||
## Shared company agent on a VPS
|
||||
|
||||
This is a valid setup when the users are in one trust boundary (for example one company team), and the agent is business-only.
|
||||
Running a single agent for a team is a valid setup when every user is in the same trust boundary and the agent is business-only.
|
||||
|
||||
- Keep it on a dedicated runtime (VPS/VM/container + dedicated OS user/accounts).
|
||||
- Do not sign that runtime into personal Apple/Google accounts or personal browser/password-manager profiles.
|
||||
- If users are adversarial to each other, split by gateway/host/OS user.
|
||||
|
||||
Security model details: [Security](/gateway/security)
|
||||
Security model details: [Security](/gateway/security).
|
||||
|
||||
## Using nodes with a VPS
|
||||
|
||||
@ -51,7 +60,7 @@ You can keep the Gateway in the cloud and pair **nodes** on your local devices
|
||||
(Mac/iOS/Android/headless). Nodes provide local screen/camera/canvas and `system.run`
|
||||
capabilities while the Gateway stays in the cloud.
|
||||
|
||||
Docs: [Nodes](/nodes), [Nodes CLI](/cli/nodes)
|
||||
Docs: [Nodes](/nodes), [Nodes CLI](/cli/nodes).
|
||||
|
||||
## Startup tuning for small VMs and ARM hosts
|
||||
|
||||
@ -68,14 +77,14 @@ source ~/.bashrc
|
||||
|
||||
- `NODE_COMPILE_CACHE` improves repeated command startup times.
|
||||
- `OPENCLAW_NO_RESPAWN=1` avoids extra startup overhead from a self-respawn path.
|
||||
- First command run warms cache; subsequent runs are faster.
|
||||
- For Raspberry Pi specifics, see [Raspberry Pi](/platforms/raspberry-pi).
|
||||
- First command run warms the cache; subsequent runs are faster.
|
||||
- For Raspberry Pi specifics, see [Raspberry Pi](/install/raspberry-pi).
|
||||
|
||||
### systemd tuning checklist (optional)
|
||||
|
||||
For VM hosts using `systemd`, consider:
|
||||
|
||||
- Add service env for stable startup path:
|
||||
- Add service env for a stable startup path:
|
||||
- `OPENCLAW_NO_RESPAWN=1`
|
||||
- `NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache`
|
||||
- Keep restart behavior explicit:
|
||||
|
||||
@ -3,12 +3,12 @@ import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { DiscordAccountConfig, OpenClawConfig } from "openclaw/plugin-sdk/discord-core";
|
||||
import {
|
||||
mergeDiscordAccountConfig,
|
||||
resolveDefaultDiscordAccountId,
|
||||
resolveDiscordAccountConfig,
|
||||
} from "./accounts.js";
|
||||
import type { DiscordAccountConfig, OpenClawConfig } from "./runtime-api.js";
|
||||
|
||||
export type DiscordCredentialStatus = "available" | "configured_unavailable" | "missing";
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import {
|
||||
createAccountListHelpers,
|
||||
normalizeAccountId,
|
||||
resolveAccountEntry,
|
||||
createAccountActionGate,
|
||||
} from "openclaw/plugin-sdk/account-resolution";
|
||||
import type { OpenClawConfig, DiscordAccountConfig, DiscordActionConfig } from "./runtime-api.js";
|
||||
} from "openclaw/plugin-sdk/account-helpers";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
||||
import type { DiscordAccountConfig, DiscordActionConfig, OpenClawConfig } from "./runtime-api.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
export type ResolvedDiscordAccount = {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { discordSetupWizard as discordSetupWizardImpl } from "./setup-surface.js";
|
||||
import { createDiscordSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
type DiscordSetupWizard = typeof import("./setup-surface.js").discordSetupWizard;
|
||||
|
||||
export const discordSetupWizard: DiscordSetupWizard = { ...discordSetupWizardImpl };
|
||||
export const discordSetupWizard: DiscordSetupWizard = createDiscordSetupWizardProxy(
|
||||
async () => (await import("./setup-surface.js")).discordSetupWizard,
|
||||
);
|
||||
|
||||
163
extensions/discord/src/setup-account-state.ts
Normal file
163
extensions/discord/src/setup-account-state.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { DiscordAccountConfig } from "./runtime-api.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
export type InspectedDiscordSetupAccount = {
|
||||
accountId: string;
|
||||
enabled: boolean;
|
||||
token: string;
|
||||
tokenSource: "env" | "config" | "none";
|
||||
tokenStatus: "available" | "configured_unavailable" | "missing";
|
||||
configured: boolean;
|
||||
config: DiscordAccountConfig;
|
||||
};
|
||||
|
||||
function resolveDiscordAccountEntry(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
): DiscordAccountConfig | undefined {
|
||||
const accounts = cfg.channels?.discord?.accounts;
|
||||
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeAccountId(accountId);
|
||||
const direct = accounts[normalized];
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
|
||||
return matchKey ? accounts[matchKey] : undefined;
|
||||
}
|
||||
|
||||
function inspectConfiguredToken(value: unknown): {
|
||||
token: string;
|
||||
tokenSource: "config";
|
||||
tokenStatus: "available" | "configured_unavailable";
|
||||
} | null {
|
||||
const normalized = normalizeSecretInputString(value);
|
||||
if (normalized) {
|
||||
return {
|
||||
token: normalized.replace(/^Bot\s+/i, ""),
|
||||
tokenSource: "config",
|
||||
tokenStatus: "available",
|
||||
};
|
||||
}
|
||||
if (hasConfiguredSecretInput(value)) {
|
||||
return {
|
||||
token: "",
|
||||
tokenSource: "config",
|
||||
tokenStatus: "configured_unavailable",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function listDiscordSetupAccountIds(cfg: OpenClawConfig): string[] {
|
||||
const accounts = cfg.channels?.discord?.accounts;
|
||||
const ids =
|
||||
accounts && typeof accounts === "object" && !Array.isArray(accounts)
|
||||
? Object.keys(accounts)
|
||||
.map((accountId) => normalizeAccountId(accountId))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
return [...new Set([DEFAULT_ACCOUNT_ID, ...ids])];
|
||||
}
|
||||
|
||||
export function resolveDefaultDiscordSetupAccountId(cfg: OpenClawConfig): string {
|
||||
return listDiscordSetupAccountIds(cfg)[0] ?? DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
export function resolveDiscordSetupAccountConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): { accountId: string; config: DiscordAccountConfig } {
|
||||
const accountId = normalizeAccountId(params.accountId ?? DEFAULT_ACCOUNT_ID);
|
||||
const { accounts: _ignored, ...base } = (params.cfg.channels?.discord ??
|
||||
{}) as DiscordAccountConfig & {
|
||||
accounts?: unknown;
|
||||
};
|
||||
return {
|
||||
accountId,
|
||||
config: {
|
||||
...base,
|
||||
...(resolveDiscordAccountEntry(params.cfg, accountId) ?? {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function inspectDiscordSetupAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): InspectedDiscordSetupAccount {
|
||||
const { accountId, config } = resolveDiscordSetupAccountConfig(params);
|
||||
const enabled = params.cfg.channels?.discord?.enabled !== false && config.enabled !== false;
|
||||
const accountConfig = resolveDiscordAccountEntry(params.cfg, accountId);
|
||||
const hasAccountToken = Boolean(
|
||||
accountConfig &&
|
||||
Object.prototype.hasOwnProperty.call(accountConfig as Record<string, unknown>, "token"),
|
||||
);
|
||||
const accountToken = inspectConfiguredToken(accountConfig?.token);
|
||||
if (accountToken) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
token: accountToken.token,
|
||||
tokenSource: accountToken.tokenSource,
|
||||
tokenStatus: accountToken.tokenStatus,
|
||||
configured: true,
|
||||
config,
|
||||
};
|
||||
}
|
||||
if (hasAccountToken) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
token: "",
|
||||
tokenSource: "none",
|
||||
tokenStatus: "missing",
|
||||
configured: false,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
const channelToken = inspectConfiguredToken(params.cfg.channels?.discord?.token);
|
||||
if (channelToken) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
token: channelToken.token,
|
||||
tokenSource: channelToken.tokenSource,
|
||||
tokenStatus: channelToken.tokenStatus,
|
||||
configured: true,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
const tokenResolution = resolveDiscordToken(params.cfg, { accountId });
|
||||
if (tokenResolution.token) {
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
token: tokenResolution.token,
|
||||
tokenSource: tokenResolution.source,
|
||||
tokenStatus: "available",
|
||||
configured: true,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
token: "",
|
||||
tokenSource: "none",
|
||||
tokenStatus: "missing",
|
||||
configured: false,
|
||||
config,
|
||||
};
|
||||
}
|
||||
@ -1,24 +1,27 @@
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import type { DiscordGuildEntry } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { createEnvPatchedAccountSetupAdapter } from "openclaw/plugin-sdk/setup-adapter-runtime";
|
||||
import type {
|
||||
ChannelSetupAdapter,
|
||||
ChannelSetupDmPolicy,
|
||||
ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import {
|
||||
inspectDiscordSetupAccount,
|
||||
listDiscordSetupAccountIds,
|
||||
resolveDiscordSetupAccountConfig,
|
||||
} from "./setup-account-state.js";
|
||||
import {
|
||||
createAccountScopedAllowFromSection,
|
||||
createAccountScopedGroupAccessSection,
|
||||
createAllowlistSetupWizardProxy,
|
||||
createLegacyCompatChannelDmPolicy,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
createEnvPatchedAccountSetupAdapter,
|
||||
parseMentionOrPrefixedId,
|
||||
patchChannelConfigForAccount,
|
||||
setSetupChannelEnabled,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
createAllowlistSetupWizardProxy,
|
||||
type ChannelSetupAdapter,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { inspectDiscordAccount } from "./account-inspect.js";
|
||||
import { listDiscordAccountIds, resolveDiscordAccount } from "./accounts.js";
|
||||
} from "./setup-runtime-helpers.js";
|
||||
|
||||
const channel = "discord" as const;
|
||||
|
||||
@ -104,8 +107,8 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
configuredScore: 2,
|
||||
unconfiguredScore: 1,
|
||||
resolveConfigured: ({ cfg }) =>
|
||||
listDiscordAccountIds(cfg).some((accountId) => {
|
||||
const account = inspectDiscordAccount({ cfg, accountId });
|
||||
listDiscordSetupAccountIds(cfg).some((accountId) => {
|
||||
const account = inspectDiscordSetupAccount({ cfg, accountId });
|
||||
return account.configured;
|
||||
}),
|
||||
},
|
||||
@ -122,7 +125,7 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
inputPrompt: "Enter Discord bot token",
|
||||
allowEnv: ({ accountId }: { accountId: string }) => accountId === DEFAULT_ACCOUNT_ID,
|
||||
inspect: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) => {
|
||||
const account = inspectDiscordAccount({ cfg, accountId });
|
||||
const account = inspectDiscordSetupAccount({ cfg, accountId });
|
||||
return {
|
||||
accountConfigured: account.configured,
|
||||
hasConfiguredValue: account.tokenStatus !== "missing",
|
||||
@ -136,25 +139,24 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
},
|
||||
],
|
||||
groupAccess: createAccountScopedGroupAccessSection({
|
||||
channel,
|
||||
label: "Discord channels",
|
||||
placeholder: "My Server/#general, guildId/channelId, #support",
|
||||
currentPolicy: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
resolveDiscordAccount({ cfg, accountId }).config.groupPolicy ?? "allowlist",
|
||||
resolveDiscordSetupAccountConfig({ cfg, accountId }).config.groupPolicy ?? "allowlist",
|
||||
currentEntries: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
Object.entries(resolveDiscordAccount({ cfg, accountId }).config.guilds ?? {}).flatMap(
|
||||
([guildKey, value]) => {
|
||||
const channels = value?.channels ?? {};
|
||||
const channelKeys = Object.keys(channels);
|
||||
if (channelKeys.length === 0) {
|
||||
const input = /^\d+$/.test(guildKey) ? `guild:${guildKey}` : guildKey;
|
||||
return [input];
|
||||
}
|
||||
return channelKeys.map((channelKey) => `${guildKey}/${channelKey}`);
|
||||
},
|
||||
),
|
||||
Object.entries(
|
||||
resolveDiscordSetupAccountConfig({ cfg, accountId }).config.guilds ?? {},
|
||||
).flatMap(([guildKey, value]) => {
|
||||
const channels = value?.channels ?? {};
|
||||
const channelKeys = Object.keys(channels);
|
||||
if (channelKeys.length === 0) {
|
||||
const input = /^\d+$/.test(guildKey) ? `guild:${guildKey}` : guildKey;
|
||||
return [input];
|
||||
}
|
||||
return channelKeys.map((channelKey) => `${guildKey}/${channelKey}`);
|
||||
}),
|
||||
updatePrompt: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
Boolean(resolveDiscordAccount({ cfg, accountId }).config.guilds),
|
||||
Boolean(resolveDiscordSetupAccountConfig({ cfg, accountId }).config.guilds),
|
||||
resolveAllowlist: handlers.resolveGroupAllowlist,
|
||||
fallbackResolved: (entries) => entries.map((input) => ({ input, resolved: false })),
|
||||
applyAllowlist: ({
|
||||
@ -168,7 +170,6 @@ export function createDiscordSetupWizardBase(handlers: {
|
||||
}) => setDiscordGuildChannelAllowlist(cfg, accountId, resolved as never),
|
||||
}),
|
||||
allowFrom: createAccountScopedAllowFromSection({
|
||||
channel,
|
||||
credentialInputKey: "token",
|
||||
helpTitle: "Discord allowlist",
|
||||
helpLines: [
|
||||
|
||||
436
extensions/discord/src/setup-runtime-helpers.ts
Normal file
436
extensions/discord/src/setup-runtime-helpers.ts
Normal file
@ -0,0 +1,436 @@
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type {
|
||||
ChannelSetupDmPolicy,
|
||||
ChannelSetupWizard,
|
||||
WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import {
|
||||
resolveDefaultDiscordSetupAccountId,
|
||||
resolveDiscordSetupAccountConfig,
|
||||
} from "./setup-account-state.js";
|
||||
|
||||
export function parseMentionOrPrefixedId(params: {
|
||||
value: string;
|
||||
mentionPattern: RegExp;
|
||||
prefixPattern?: RegExp;
|
||||
idPattern: RegExp;
|
||||
normalizeId?: (id: string) => string;
|
||||
}): string | null {
|
||||
const trimmed = params.value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const mentionMatch = trimmed.match(params.mentionPattern);
|
||||
if (mentionMatch?.[1]) {
|
||||
return params.normalizeId ? params.normalizeId(mentionMatch[1]) : mentionMatch[1];
|
||||
}
|
||||
if (params.prefixPattern?.test(trimmed)) {
|
||||
const stripped = trimmed.replace(params.prefixPattern, "").trim();
|
||||
if (!stripped || !params.idPattern.test(stripped)) {
|
||||
return null;
|
||||
}
|
||||
return params.normalizeId ? params.normalizeId(stripped) : stripped;
|
||||
}
|
||||
if (!params.idPattern.test(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
return params.normalizeId ? params.normalizeId(trimmed) : trimmed;
|
||||
}
|
||||
|
||||
function splitSetupEntries(raw: string): string[] {
|
||||
return raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function mergeAllowFromEntries(
|
||||
current: Array<string | number> | null | undefined,
|
||||
additions: Array<string | number>,
|
||||
): string[] {
|
||||
const merged = [...(current ?? []), ...additions]
|
||||
.map((value) => String(value).trim())
|
||||
.filter(Boolean);
|
||||
return [...new Set(merged)];
|
||||
}
|
||||
|
||||
function patchDiscordChannelConfigForAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
patch: Record<string, unknown>;
|
||||
}): OpenClawConfig {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const channelConfig = (params.cfg.channels?.discord as Record<string, unknown> | undefined) ?? {};
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
discord: {
|
||||
...channelConfig,
|
||||
...params.patch,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const accounts =
|
||||
(channelConfig.accounts as Record<string, Record<string, unknown>> | undefined) ?? {};
|
||||
const accountConfig = accounts[accountId] ?? {};
|
||||
return {
|
||||
...params.cfg,
|
||||
channels: {
|
||||
...params.cfg.channels,
|
||||
discord: {
|
||||
...channelConfig,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...accounts,
|
||||
[accountId]: {
|
||||
...accountConfig,
|
||||
...params.patch,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function setSetupChannelEnabled(
|
||||
cfg: OpenClawConfig,
|
||||
channel: string,
|
||||
enabled: boolean,
|
||||
): OpenClawConfig {
|
||||
const channelConfig = (cfg.channels?.[channel] as Record<string, unknown> | undefined) ?? {};
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
[channel]: {
|
||||
...channelConfig,
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function patchChannelConfigForAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: "discord";
|
||||
accountId: string;
|
||||
patch: Record<string, unknown>;
|
||||
}): OpenClawConfig {
|
||||
return patchDiscordChannelConfigForAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
patch: params.patch,
|
||||
});
|
||||
}
|
||||
|
||||
export function createLegacyCompatChannelDmPolicy(params: {
|
||||
label: string;
|
||||
channel: "discord";
|
||||
promptAllowFrom?: ChannelSetupDmPolicy["promptAllowFrom"];
|
||||
}): ChannelSetupDmPolicy {
|
||||
return {
|
||||
label: params.label,
|
||||
channel: params.channel,
|
||||
policyKey: `channels.${params.channel}.dmPolicy`,
|
||||
allowFromKey: `channels.${params.channel}.allowFrom`,
|
||||
getCurrent: (cfg) =>
|
||||
(
|
||||
cfg.channels?.[params.channel] as
|
||||
| {
|
||||
dmPolicy?: "open" | "pairing" | "allowlist";
|
||||
dm?: { policy?: "open" | "pairing" | "allowlist" };
|
||||
}
|
||||
| undefined
|
||||
)?.dmPolicy ??
|
||||
(
|
||||
cfg.channels?.[params.channel] as
|
||||
| {
|
||||
dmPolicy?: "open" | "pairing" | "allowlist";
|
||||
dm?: { policy?: "open" | "pairing" | "allowlist" };
|
||||
}
|
||||
| undefined
|
||||
)?.dm?.policy ??
|
||||
"pairing",
|
||||
setPolicy: (cfg, policy) =>
|
||||
patchDiscordChannelConfigForAccount({
|
||||
cfg,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
patch: {
|
||||
dmPolicy: policy,
|
||||
...(policy === "open"
|
||||
? {
|
||||
allowFrom: [
|
||||
...new Set(
|
||||
[
|
||||
...(((
|
||||
cfg.channels?.discord as { allowFrom?: Array<string | number> } | undefined
|
||||
)?.allowFrom ?? []) as Array<string | number>),
|
||||
"*",
|
||||
]
|
||||
.map((value) => String(value).trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
...(params.promptAllowFrom ? { promptAllowFrom: params.promptAllowFrom } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function noteChannelLookupFailure(params: {
|
||||
prompter: Pick<WizardPrompter, "note">;
|
||||
label: string;
|
||||
error: unknown;
|
||||
}) {
|
||||
await params.prompter.note(
|
||||
`Channel lookup failed; keeping entries as typed. ${String(params.error)}`,
|
||||
params.label,
|
||||
);
|
||||
}
|
||||
|
||||
export function createAccountScopedAllowFromSection(params: {
|
||||
credentialInputKey?: NonNullable<ChannelSetupWizard["allowFrom"]>["credentialInputKey"];
|
||||
helpTitle?: string;
|
||||
helpLines?: string[];
|
||||
message: string;
|
||||
placeholder: string;
|
||||
invalidWithoutCredentialNote: string;
|
||||
parseId: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["parseId"]>;
|
||||
resolveEntries: NonNullable<NonNullable<ChannelSetupWizard["allowFrom"]>["resolveEntries"]>;
|
||||
}): NonNullable<ChannelSetupWizard["allowFrom"]> {
|
||||
return {
|
||||
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
||||
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
||||
...(params.credentialInputKey ? { credentialInputKey: params.credentialInputKey } : {}),
|
||||
message: params.message,
|
||||
placeholder: params.placeholder,
|
||||
invalidWithoutCredentialNote: params.invalidWithoutCredentialNote,
|
||||
parseId: params.parseId,
|
||||
resolveEntries: params.resolveEntries,
|
||||
apply: ({ cfg, accountId, allowFrom }) =>
|
||||
patchDiscordChannelConfigForAccount({
|
||||
cfg,
|
||||
accountId,
|
||||
patch: { dmPolicy: "allowlist", allowFrom },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createAccountScopedGroupAccessSection<TResolved>(params: {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
helpTitle?: string;
|
||||
helpLines?: string[];
|
||||
skipAllowlistEntries?: boolean;
|
||||
currentPolicy: NonNullable<ChannelSetupWizard["groupAccess"]>["currentPolicy"];
|
||||
currentEntries: NonNullable<ChannelSetupWizard["groupAccess"]>["currentEntries"];
|
||||
updatePrompt: NonNullable<ChannelSetupWizard["groupAccess"]>["updatePrompt"];
|
||||
resolveAllowlist?: NonNullable<
|
||||
NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]
|
||||
>;
|
||||
fallbackResolved: (entries: string[]) => TResolved;
|
||||
applyAllowlist: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
resolved: TResolved;
|
||||
}) => OpenClawConfig;
|
||||
}): NonNullable<ChannelSetupWizard["groupAccess"]> {
|
||||
return {
|
||||
label: params.label,
|
||||
placeholder: params.placeholder,
|
||||
...(params.helpTitle ? { helpTitle: params.helpTitle } : {}),
|
||||
...(params.helpLines ? { helpLines: params.helpLines } : {}),
|
||||
...(params.skipAllowlistEntries ? { skipAllowlistEntries: true } : {}),
|
||||
currentPolicy: params.currentPolicy,
|
||||
currentEntries: params.currentEntries,
|
||||
updatePrompt: params.updatePrompt,
|
||||
setPolicy: ({ cfg, accountId, policy }) =>
|
||||
patchDiscordChannelConfigForAccount({
|
||||
cfg,
|
||||
accountId,
|
||||
patch: { groupPolicy: policy },
|
||||
}),
|
||||
...(params.resolveAllowlist
|
||||
? {
|
||||
resolveAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => {
|
||||
try {
|
||||
return await params.resolveAllowlist!({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
});
|
||||
} catch (error) {
|
||||
await noteChannelLookupFailure({
|
||||
prompter,
|
||||
label: params.label,
|
||||
error,
|
||||
});
|
||||
return params.fallbackResolved(entries);
|
||||
}
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
applyAllowlist: ({ cfg, accountId, resolved }) =>
|
||||
params.applyAllowlist({
|
||||
cfg,
|
||||
accountId,
|
||||
resolved: resolved as TResolved,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createAllowlistSetupWizardProxy<TGroupResolved>(params: {
|
||||
loadWizard: () => Promise<ChannelSetupWizard>;
|
||||
createBase: (handlers: {
|
||||
promptAllowFrom: NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>;
|
||||
resolveAllowFromEntries: NonNullable<
|
||||
NonNullable<ChannelSetupWizard["allowFrom"]>["resolveEntries"]
|
||||
>;
|
||||
resolveGroupAllowlist: NonNullable<
|
||||
NonNullable<NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]>
|
||||
>;
|
||||
}) => ChannelSetupWizard;
|
||||
fallbackResolvedGroupAllowlist: (entries: string[]) => TGroupResolved;
|
||||
}) {
|
||||
return params.createBase({
|
||||
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
||||
const wizard = await params.loadWizard();
|
||||
if (!wizard.dmPolicy?.promptAllowFrom) {
|
||||
return cfg;
|
||||
}
|
||||
return await wizard.dmPolicy.promptAllowFrom({ cfg, prompter, accountId });
|
||||
},
|
||||
resolveAllowFromEntries: async ({ cfg, accountId, credentialValues, entries }) => {
|
||||
const wizard = await params.loadWizard();
|
||||
if (!wizard.allowFrom) {
|
||||
return entries.map((input) => ({ input, resolved: false, id: null }));
|
||||
}
|
||||
return await wizard.allowFrom.resolveEntries({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
});
|
||||
},
|
||||
resolveGroupAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => {
|
||||
const wizard = await params.loadWizard();
|
||||
if (!wizard.groupAccess?.resolveAllowlist) {
|
||||
return params.fallbackResolvedGroupAllowlist(entries) as Awaited<
|
||||
ReturnType<
|
||||
NonNullable<NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]>
|
||||
>
|
||||
>;
|
||||
}
|
||||
return (await wizard.groupAccess.resolveAllowlist({
|
||||
cfg,
|
||||
accountId,
|
||||
credentialValues,
|
||||
entries,
|
||||
prompter,
|
||||
})) as Awaited<
|
||||
ReturnType<NonNullable<NonNullable<ChannelSetupWizard["groupAccess"]>["resolveAllowlist"]>>
|
||||
>;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveEntriesWithOptionalToken<TResult>(params: {
|
||||
token?: string | null;
|
||||
entries: string[];
|
||||
buildWithoutToken: (input: string) => TResult;
|
||||
resolveEntries: (params: { token: string; entries: string[] }) => Promise<TResult[]>;
|
||||
}): Promise<TResult[]> {
|
||||
const token = params.token?.trim();
|
||||
if (!token) {
|
||||
return params.entries.map(params.buildWithoutToken);
|
||||
}
|
||||
return await params.resolveEntries({
|
||||
token,
|
||||
entries: params.entries,
|
||||
});
|
||||
}
|
||||
|
||||
export async function promptLegacyChannelAllowFromForAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
noteTitle: string;
|
||||
noteLines: string[];
|
||||
message: string;
|
||||
placeholder: string;
|
||||
parseId: (value: string) => string | null;
|
||||
invalidWithoutTokenNote: string;
|
||||
resolveEntries: (params: {
|
||||
token: string;
|
||||
entries: string[];
|
||||
}) => Promise<Array<{ input: string; resolved: boolean; id?: string | null }>>;
|
||||
resolveToken: (accountId: string) => string | null | undefined;
|
||||
resolveExisting: (accountId: string, cfg: OpenClawConfig) => Array<string | number>;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? resolveDefaultDiscordSetupAccountId(params.cfg),
|
||||
);
|
||||
await params.prompter.note(params.noteLines.join("\n"), params.noteTitle);
|
||||
const token = params.resolveToken(accountId);
|
||||
const existing = params.resolveExisting(accountId, params.cfg);
|
||||
|
||||
while (true) {
|
||||
const entry = await params.prompter.text({
|
||||
message: params.message,
|
||||
placeholder: params.placeholder,
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
const parts = splitSetupEntries(String(entry));
|
||||
if (!token) {
|
||||
const ids = parts.map(params.parseId).filter(Boolean) as string[];
|
||||
if (ids.length !== parts.length) {
|
||||
await params.prompter.note(params.invalidWithoutTokenNote, params.noteTitle);
|
||||
continue;
|
||||
}
|
||||
return patchDiscordChannelConfigForAccount({
|
||||
cfg: params.cfg,
|
||||
accountId,
|
||||
patch: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: mergeAllowFromEntries(existing, ids),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const results = await params.resolveEntries({ token, entries: parts }).catch(() => null);
|
||||
if (!results) {
|
||||
await params.prompter.note("Failed to resolve usernames. Try again.", params.noteTitle);
|
||||
continue;
|
||||
}
|
||||
const unresolved = results.filter((result) => !result.resolved || !result.id);
|
||||
if (unresolved.length > 0) {
|
||||
await params.prompter.note(
|
||||
`Could not resolve: ${unresolved.map((result) => result.input).join(", ")}`,
|
||||
params.noteTitle,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
return patchDiscordChannelConfigForAccount({
|
||||
cfg: params.cfg,
|
||||
accountId,
|
||||
patch: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: mergeAllowFromEntries(
|
||||
existing,
|
||||
results.map((result) => result.id as string),
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,26 @@
|
||||
import {
|
||||
resolveEntriesWithOptionalToken,
|
||||
type OpenClawConfig,
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
type ChannelSetupWizard,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { resolveDefaultDiscordAccountId, resolveDiscordAccount } from "./accounts.js";
|
||||
import { resolveDiscordChannelAllowlist } from "./resolve-channels.js";
|
||||
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
|
||||
import {
|
||||
resolveDefaultDiscordSetupAccountId,
|
||||
resolveDiscordSetupAccountConfig,
|
||||
} from "./setup-account-state.js";
|
||||
import {
|
||||
createDiscordSetupWizardBase,
|
||||
DISCORD_TOKEN_HELP_LINES,
|
||||
parseDiscordAllowFromId,
|
||||
setDiscordGuildChannelAllowlist,
|
||||
} from "./setup-core.js";
|
||||
import {
|
||||
promptLegacyChannelAllowFromForAccount,
|
||||
resolveEntriesWithOptionalToken,
|
||||
} from "./setup-runtime-helpers.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
const channel = "discord" as const;
|
||||
|
||||
@ -48,13 +54,8 @@ async function promptDiscordAllowFrom(params: {
|
||||
}): Promise<OpenClawConfig> {
|
||||
return await promptLegacyChannelAllowFromForAccount({
|
||||
cfg: params.cfg,
|
||||
channel,
|
||||
prompter: params.prompter,
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultDiscordAccountId(params.cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
|
||||
resolveExisting: (account) => account.config.allowFrom ?? account.config.dm?.allowFrom ?? [],
|
||||
resolveToken: (account) => account.token,
|
||||
noteTitle: "Discord allowlist",
|
||||
noteLines: [
|
||||
"Allowlist Discord DMs by username (we resolve to user ids).",
|
||||
@ -69,6 +70,11 @@ async function promptDiscordAllowFrom(params: {
|
||||
placeholder: "@alice, 123456789012345678",
|
||||
parseId: parseDiscordAllowFromId,
|
||||
invalidWithoutTokenNote: "Bot token missing; use numeric user ids (or mention form) only.",
|
||||
resolveExisting: (accountId, cfg) => {
|
||||
const account = resolveDiscordSetupAccountConfig({ cfg, accountId }).config;
|
||||
return account.allowFrom ?? account.dm?.allowFrom ?? [];
|
||||
},
|
||||
resolveToken: (accountId) => resolveDiscordToken(params.cfg, { accountId }).token,
|
||||
resolveEntries: async ({ token, entries }) =>
|
||||
(
|
||||
await resolveDiscordUserAllowlist({
|
||||
@ -91,7 +97,7 @@ async function resolveDiscordGroupAllowlist(params: {
|
||||
}) {
|
||||
return await resolveEntriesWithOptionalToken({
|
||||
token:
|
||||
resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId }).token ||
|
||||
resolveDiscordToken(params.cfg, { accountId: params.accountId }).token ||
|
||||
(typeof params.credentialValues.token === "string" ? params.credentialValues.token : ""),
|
||||
entries: params.entries,
|
||||
buildWithoutToken: (input) => ({
|
||||
@ -111,7 +117,7 @@ export const discordSetupWizard: ChannelSetupWizard = createDiscordSetupWizardBa
|
||||
resolveAllowFromEntries: async ({ cfg, accountId, credentialValues, entries }) =>
|
||||
await resolveDiscordAllowFromEntries({
|
||||
token:
|
||||
resolveDiscordAccount({ cfg, accountId }).token ||
|
||||
resolveDiscordToken(cfg, { accountId }).token ||
|
||||
(typeof credentialValues.token === "string" ? credentialValues.token : ""),
|
||||
entries,
|
||||
}),
|
||||
|
||||
@ -544,6 +544,15 @@ function registerEventHandlers(
|
||||
}),
|
||||
},
|
||||
};
|
||||
const syntheticMessageId = syntheticEvent.message.message_id;
|
||||
if (await hasProcessedFeishuMessage(syntheticMessageId, accountId, log)) {
|
||||
log(`feishu[${accountId}]: dropping duplicate bot-menu event for ${syntheticMessageId}`);
|
||||
return;
|
||||
}
|
||||
if (!tryBeginFeishuMessageProcessing(syntheticMessageId, accountId)) {
|
||||
log(`feishu[${accountId}]: dropping in-flight bot-menu event for ${syntheticMessageId}`);
|
||||
return;
|
||||
}
|
||||
const handleLegacyMenu = () =>
|
||||
handleFeishuMessage({
|
||||
cfg,
|
||||
@ -553,6 +562,7 @@ function registerEventHandlers(
|
||||
runtime,
|
||||
chatHistories,
|
||||
accountId,
|
||||
processingClaimHeld: true,
|
||||
});
|
||||
|
||||
const promise = maybeHandleFeishuQuickActionMenu({
|
||||
@ -561,12 +571,19 @@ function registerEventHandlers(
|
||||
operatorOpenId,
|
||||
runtime,
|
||||
accountId,
|
||||
}).then((handledMenu) => {
|
||||
if (handledMenu) {
|
||||
return;
|
||||
}
|
||||
return handleLegacyMenu();
|
||||
});
|
||||
})
|
||||
.then(async (handledMenu) => {
|
||||
if (handledMenu) {
|
||||
await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
|
||||
releaseFeishuMessageProcessing(syntheticMessageId, accountId);
|
||||
return;
|
||||
}
|
||||
return await handleLegacyMenu();
|
||||
})
|
||||
.catch((err) => {
|
||||
releaseFeishuMessageProcessing(syntheticMessageId, accountId);
|
||||
throw err;
|
||||
});
|
||||
if (fireAndForget) {
|
||||
promise.catch((err) => {
|
||||
error(`feishu[${accountId}]: error handling bot menu event: ${String(err)}`);
|
||||
|
||||
356
extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts
Normal file
356
extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts
Normal file
@ -0,0 +1,356 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import { monitorSingleAccount } from "./monitor.account.js";
|
||||
import { setFeishuRuntime } from "./runtime.js";
|
||||
import type { ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
const createEventDispatcherMock = vi.hoisted(() => vi.fn());
|
||||
const monitorWebSocketMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const monitorWebhookMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const createFeishuThreadBindingManagerMock = vi.hoisted(() => vi.fn(() => ({ stop: vi.fn() })));
|
||||
const createFeishuReplyDispatcherMock = vi.hoisted(() => vi.fn());
|
||||
const resolveBoundConversationMock = vi.hoisted(() => vi.fn(() => null));
|
||||
const touchBindingMock = vi.hoisted(() => vi.fn());
|
||||
const resolveAgentRouteMock = vi.hoisted(() => vi.fn());
|
||||
const dispatchReplyFromConfigMock = vi.hoisted(() => vi.fn());
|
||||
const withReplyDispatcherMock = vi.hoisted(() => vi.fn());
|
||||
const finalizeInboundContextMock = vi.hoisted(() => vi.fn((ctx) => ctx));
|
||||
const sendCardFeishuMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({ messageId: "om_card_sent", chatId: "p2p:ou_user1" })),
|
||||
);
|
||||
const getMessageFeishuMock = vi.hoisted(() => vi.fn(async () => null));
|
||||
const listFeishuThreadMessagesMock = vi.hoisted(() => vi.fn(async () => []));
|
||||
const sendMessageFeishuMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({ messageId: "om_sent", chatId: "p2p:ou_user1" })),
|
||||
);
|
||||
|
||||
let handlers: Record<string, (data: unknown) => Promise<void>> = {};
|
||||
let lastRuntime: RuntimeEnv | null = null;
|
||||
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
|
||||
vi.mock("./client.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./client.js")>("./client.js");
|
||||
return {
|
||||
...actual,
|
||||
createEventDispatcher: createEventDispatcherMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./monitor.transport.js", () => ({
|
||||
monitorWebSocket: monitorWebSocketMock,
|
||||
monitorWebhook: monitorWebhookMock,
|
||||
}));
|
||||
|
||||
vi.mock("./thread-bindings.js", () => ({
|
||||
createFeishuThreadBindingManager: createFeishuThreadBindingManagerMock,
|
||||
}));
|
||||
|
||||
vi.mock("./reply-dispatcher.js", () => ({
|
||||
createFeishuReplyDispatcher: createFeishuReplyDispatcherMock,
|
||||
}));
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
sendCardFeishu: sendCardFeishuMock,
|
||||
getMessageFeishu: getMessageFeishuMock,
|
||||
listFeishuThreadMessages: listFeishuThreadMessagesMock,
|
||||
sendMessageFeishu: sendMessageFeishuMock,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
getSessionBindingService: () => ({
|
||||
resolveByConversation: resolveBoundConversationMock,
|
||||
touch: touchBindingMock,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../../src/infra/outbound/session-binding-service.js", () => ({
|
||||
getSessionBindingService: () => ({
|
||||
resolveByConversation: resolveBoundConversationMock,
|
||||
touch: touchBindingMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
function createLifecycleConfig(): ClawdbotConfig {
|
||||
return {
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
dmPolicy: "open",
|
||||
requireMention: false,
|
||||
resolveSenderNames: false,
|
||||
accounts: {
|
||||
"acct-menu": {
|
||||
enabled: true,
|
||||
appId: "cli_test",
|
||||
appSecret: "secret_test", // pragma: allowlist secret
|
||||
connectionMode: "websocket",
|
||||
dmPolicy: "open",
|
||||
requireMention: false,
|
||||
resolveSenderNames: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
inbound: {
|
||||
debounceMs: 0,
|
||||
byChannel: {
|
||||
feishu: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
}
|
||||
|
||||
function createLifecycleAccount(): ResolvedFeishuAccount {
|
||||
return {
|
||||
accountId: "acct-menu",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
appId: "cli_test",
|
||||
appSecret: "secret_test", // pragma: allowlist secret
|
||||
domain: "feishu",
|
||||
config: {
|
||||
enabled: true,
|
||||
connectionMode: "websocket",
|
||||
dmPolicy: "open",
|
||||
requireMention: false,
|
||||
resolveSenderNames: false,
|
||||
},
|
||||
} as ResolvedFeishuAccount;
|
||||
}
|
||||
|
||||
function createRuntimeEnv(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as RuntimeEnv;
|
||||
}
|
||||
|
||||
function createBotMenuEvent(params: { eventKey: string; timestamp: string }) {
|
||||
return {
|
||||
event_key: params.eventKey,
|
||||
timestamp: params.timestamp,
|
||||
operator: {
|
||||
operator_id: {
|
||||
open_id: "ou_user1",
|
||||
user_id: "user_1",
|
||||
union_id: "union_1",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function settleAsyncWork(): Promise<void> {
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
||||
async function setupLifecycleMonitor() {
|
||||
const register = vi.fn((registered: Record<string, (data: unknown) => Promise<void>>) => {
|
||||
handlers = registered;
|
||||
});
|
||||
createEventDispatcherMock.mockReturnValue({ register });
|
||||
|
||||
lastRuntime = createRuntimeEnv();
|
||||
|
||||
await monitorSingleAccount({
|
||||
cfg: createLifecycleConfig(),
|
||||
account: createLifecycleAccount(),
|
||||
runtime: lastRuntime,
|
||||
botOpenIdSource: {
|
||||
kind: "prefetched",
|
||||
botOpenId: "ou_bot_1",
|
||||
botName: "Bot",
|
||||
},
|
||||
});
|
||||
|
||||
const onBotMenu = handlers["application.bot.menu_v6"];
|
||||
if (!onBotMenu) {
|
||||
throw new Error("missing application.bot.menu_v6 handler");
|
||||
}
|
||||
return onBotMenu;
|
||||
}
|
||||
|
||||
describe("Feishu bot-menu lifecycle", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
handlers = {};
|
||||
lastRuntime = null;
|
||||
process.env.OPENCLAW_STATE_DIR = `/tmp/openclaw-feishu-bot-menu-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
const dispatcher = {
|
||||
sendToolResult: vi.fn(() => false),
|
||||
sendBlockReply: vi.fn(() => false),
|
||||
sendFinalReply: vi.fn(async () => true),
|
||||
waitForIdle: vi.fn(async () => {}),
|
||||
getQueuedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 0 })),
|
||||
markComplete: vi.fn(),
|
||||
};
|
||||
|
||||
createFeishuReplyDispatcherMock.mockReturnValue({
|
||||
dispatcher,
|
||||
replyOptions: {},
|
||||
markDispatchIdle: vi.fn(),
|
||||
});
|
||||
|
||||
resolveBoundConversationMock.mockReturnValue({
|
||||
bindingId: "binding-menu",
|
||||
targetSessionKey: "agent:bound-agent:feishu:direct:ou_user1",
|
||||
});
|
||||
|
||||
resolveAgentRouteMock.mockReturnValue({
|
||||
agentId: "main",
|
||||
channel: "feishu",
|
||||
accountId: "acct-menu",
|
||||
sessionKey: "agent:main:feishu:direct:ou_user1",
|
||||
mainSessionKey: "agent:main:main",
|
||||
matchedBy: "default",
|
||||
});
|
||||
|
||||
dispatchReplyFromConfigMock.mockImplementation(async ({ dispatcher }) => {
|
||||
await dispatcher.sendFinalReply({ text: "menu reply once" });
|
||||
return {
|
||||
queuedFinal: false,
|
||||
counts: { final: 1 },
|
||||
};
|
||||
});
|
||||
|
||||
withReplyDispatcherMock.mockImplementation(async ({ run }) => await run());
|
||||
|
||||
setFeishuRuntime(
|
||||
createPluginRuntimeMock({
|
||||
channel: {
|
||||
debounce: {
|
||||
resolveInboundDebounceMs: vi.fn(() => 0),
|
||||
createInboundDebouncer: <T>(params: {
|
||||
onFlush?: (items: T[]) => Promise<void>;
|
||||
onError?: (err: unknown, items: T[]) => void;
|
||||
}) => ({
|
||||
enqueue: async (item: T) => {
|
||||
try {
|
||||
await params.onFlush?.([item]);
|
||||
} catch (err) {
|
||||
params.onError?.(err, [item]);
|
||||
}
|
||||
},
|
||||
flushKey: async () => {},
|
||||
}),
|
||||
},
|
||||
text: {
|
||||
hasControlCommand: vi.fn(() => false),
|
||||
},
|
||||
routing: {
|
||||
resolveAgentRoute:
|
||||
resolveAgentRouteMock as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
|
||||
},
|
||||
reply: {
|
||||
resolveEnvelopeFormatOptions: vi.fn(() => ({})),
|
||||
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),
|
||||
finalizeInboundContext:
|
||||
finalizeInboundContextMock as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
||||
dispatchReplyFromConfig:
|
||||
dispatchReplyFromConfigMock as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
|
||||
withReplyDispatcher:
|
||||
withReplyDispatcherMock as unknown as PluginRuntime["channel"]["reply"]["withReplyDispatcher"],
|
||||
},
|
||||
commands: {
|
||||
shouldComputeCommandAuthorized: vi.fn(() => false),
|
||||
resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false),
|
||||
},
|
||||
session: {
|
||||
readSessionUpdatedAt: vi.fn(),
|
||||
resolveStorePath: vi.fn(() => "/tmp/feishu-bot-menu-sessions.json"),
|
||||
},
|
||||
pairing: {
|
||||
readAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||
upsertPairingRequest: vi.fn(),
|
||||
buildPairingReply: vi.fn(),
|
||||
},
|
||||
},
|
||||
media: {
|
||||
detectMime: vi.fn(async () => "text/plain"),
|
||||
},
|
||||
}) as unknown as PluginRuntime,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalStateDir === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
return;
|
||||
}
|
||||
process.env.OPENCLAW_STATE_DIR = originalStateDir;
|
||||
});
|
||||
|
||||
it("opens one launcher card across duplicate quick-actions replay", async () => {
|
||||
const onBotMenu = await setupLifecycleMonitor();
|
||||
const event = createBotMenuEvent({
|
||||
eventKey: "quick-actions",
|
||||
timestamp: "1700000000000",
|
||||
});
|
||||
|
||||
await onBotMenu(event);
|
||||
await settleAsyncWork();
|
||||
await onBotMenu(event);
|
||||
await settleAsyncWork();
|
||||
|
||||
expect(lastRuntime?.error).not.toHaveBeenCalled();
|
||||
expect(sendCardFeishuMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendCardFeishuMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
accountId: "acct-menu",
|
||||
to: "user:ou_user1",
|
||||
}),
|
||||
);
|
||||
expect(dispatchReplyFromConfigMock).not.toHaveBeenCalled();
|
||||
expect(createFeishuReplyDispatcherMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back once to the legacy routed reply path when launcher rendering fails", async () => {
|
||||
const onBotMenu = await setupLifecycleMonitor();
|
||||
const event = createBotMenuEvent({
|
||||
eventKey: "quick-actions",
|
||||
timestamp: "1700000000001",
|
||||
});
|
||||
sendCardFeishuMock.mockRejectedValueOnce(new Error("boom"));
|
||||
|
||||
await onBotMenu(event);
|
||||
await settleAsyncWork();
|
||||
await onBotMenu(event);
|
||||
await settleAsyncWork();
|
||||
|
||||
expect(lastRuntime?.error).not.toHaveBeenCalled();
|
||||
expect(sendCardFeishuMock).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchReplyFromConfigMock).toHaveBeenCalledTimes(1);
|
||||
expect(createFeishuReplyDispatcherMock).toHaveBeenCalledTimes(1);
|
||||
expect(createFeishuReplyDispatcherMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
accountId: "acct-menu",
|
||||
chatId: "p2p:ou_user1",
|
||||
replyToMessageId: "bot-menu:quick-actions:1700000000001",
|
||||
}),
|
||||
);
|
||||
expect(finalizeInboundContextMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
AccountId: "acct-menu",
|
||||
SessionKey: "agent:bound-agent:feishu:direct:ou_user1",
|
||||
MessageSid: "bot-menu:quick-actions:1700000000001",
|
||||
}),
|
||||
);
|
||||
expect(touchBindingMock).toHaveBeenCalledWith("binding-menu");
|
||||
|
||||
const dispatcher = createFeishuReplyDispatcherMock.mock.results[0]?.value.dispatcher as {
|
||||
sendFinalReply: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user