diff --git a/.gitignore b/.gitignore index 1c3b6c8..614348f 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,9 @@ packages/claw-plugin/static/ .env.local .env.*.local +# Docker install state +docker/data/ + # Logs *.log diff --git a/README.md b/README.md index 5fec937..5249aed 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,24 @@ powershell -c "irm https://openui.com/openclaw-os/install.ps1 | iex" The installer downloads the latest source, builds the workspace UI, registers it as an OpenClaw plugin, restarts the gateway, and opens the dashboard in your browser. +Docker alternative: + +```bash +cd docker +./scripts/init-env.sh +docker compose up -d --build +``` + +Windows PowerShell: + +```powershell +cd docker +.\scripts\init-env.ps1 +docker compose up -d --build +``` + +The Docker install runs OpenClaw, preloads the OpenClaw OS plugin, and reads model/API-key settings from [`docker/openclaw-os.yaml`](./docker/openclaw-os.yaml). See [`docker/README.md`](./docker/README.md) for setup details, Windows notes, and the wrapper CLI commands. + or through published package ```bash diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..99090b0 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,7 @@ +.git +.DS_Store +.env +data +node_modules +npm-debug.log +Dockerfile~ diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..df9a840 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,32 @@ +# Copy to .env or run ./scripts/init-env.sh. +OPENCLAW_VERSION=2026.5.27 +OPENCLAW_OS_PLUGIN_VERSION=0.1.5 + +# The helper script generates a random value. Keep this private. +OPENCLAW_GATEWAY_TOKEN= + +# Host ports. The gateway serves both Control UI and OpenClaw OS on this port. +OPENCLAW_GATEWAY_HOST_PORT=18789 +OPENCLAW_BRIDGE_HOST_PORT=18790 +OPENCLAW_MSTEAMS_HOST_PORT=3978 + +# Persistent host state. +OPENCLAW_CONFIG_DIR=./data/openclaw +OPENCLAW_AUTH_PROFILE_SECRET_DIR=./data/openclaw-auth-profile-secrets + +# Gateway/container behavior. +OPENCLAW_GATEWAY_BIND=lan +OPENCLAW_GATEWAY_AUTH_MODE=token +OPENCLAW_DISABLE_BONJOUR=1 +OPENCLAW_TZ=UTC + +# Optional comma-separated origins if you reverse-proxy the gateway. +OPENCLAW_CONTROL_UI_EXTRA_ORIGINS= + +# Optional provider keys. You can reference these from openclaw-os.yaml, +# for example: apiKeys.openai: ${OPENAI_API_KEY} +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +OPENROUTER_API_KEY= +GOOGLE_API_KEY= +GEMINI_API_KEY= diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..479ea46 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,63 @@ +ARG NODE_IMAGE=node:24-bookworm-slim + +FROM ${NODE_IMAGE} + +ARG OPENCLAW_VERSION=2026.5.27 +ARG OPENCLAW_OS_PLUGIN_VERSION=0.1.5 + +LABEL org.opencontainers.image.title="OpenClaw with OpenClaw OS" \ + org.opencontainers.image.description="OpenClaw gateway image with the OpenClaw OS workspace plugin pre-cached" \ + org.opencontainers.image.source="https://github.com/thesysdev/openclaw-os" \ + org.opencontainers.image.documentation="https://www.openui.com/openclaw-os" \ + org.opencontainers.image.licenses="MIT" + +ENV NODE_ENV=production \ + HOME=/home/node \ + OPENCLAW_HOME=/home/node \ + OPENCLAW_STATE_DIR=/home/node/.openclaw \ + OPENCLAW_CONFIG_DIR=/home/node/.openclaw \ + OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json \ + OPENCLAW_WORKSPACE_DIR=/home/node/.openclaw/workspace \ + OPENCLAW_OS_PLUGIN_PATH=/opt/openclaw-os-plugin/node_modules/@openuidev/openclaw-os-plugin \ + NPM_CONFIG_AUDIT=false \ + NPM_CONFIG_FUND=false + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + hostname \ + lsof \ + openssl \ + procps \ + python3 \ + python3-yaml \ + tini \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install -g "openclaw@${OPENCLAW_VERSION}" \ + && npm install --prefix /opt/openclaw-os-plugin --omit=dev "@openuidev/openclaw-os-plugin@${OPENCLAW_OS_PLUGIN_VERSION}" \ + && npm cache clean --force \ + && chown -R node:node /opt/openclaw-os-plugin + +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh + +RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh \ + && install -d -m 0755 -o node -g node /home/node/.npm \ + && install -d -m 0755 -o node -g node /home/node/.config \ + && install -d -m 0700 -o node -g node \ + /home/node/.openclaw \ + /home/node/.openclaw/workspace \ + /home/node/.config/openclaw + +USER node +WORKDIR /home/node + +EXPOSE 18789 18790 3978 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \ + CMD node -e "fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +ENTRYPOINT ["tini", "-s", "--", "docker-entrypoint.sh"] +CMD ["gateway"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..6bcbb39 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,124 @@ +# OpenClaw OS Docker + +Docker image and Compose setup for running OpenClaw with OpenClaw OS registered as a gateway plugin. This is useful when the native installer is not a good fit, especially on Windows. + +OpenClaw OS is served by the OpenClaw gateway at: + +```text +http://localhost:18789/plugins/openclawos +``` + +## Quick Start + +From this `docker/` directory: + +```bash +./scripts/init-env.sh +docker compose up -d --build +``` + +Windows PowerShell: + +```powershell +.\scripts\init-env.ps1 +docker compose up -d --build +``` + +Wait until the gateway is healthy: + +```bash +docker compose ps +``` + +The `openclaw-gateway` row should say `healthy`. First startup can take 20-60 seconds because the gateway installs/registers the OpenClaw OS plugin. + +Then open: + +- OpenClaw OS: `http://localhost:18789/plugins/openclawos` +- OpenClaw Control UI: `http://localhost:18789/` + +Use the `OPENCLAW_GATEWAY_TOKEN` from `.env` when the UI asks for the gateway token. + +If the browser says the page is empty or unavailable, the gateway is probably still starting or has crashed. Check: + +```bash +docker compose ps +docker compose logs --tail=100 openclaw-gateway +``` + +## Configure Before Running + +Edit `openclaw-os.yaml` before starting the container: + +```yaml +model: + primary: openai/gpt-5.5 + thinking: medium + +apiKeys: + openai: sk-your-key-here +``` + +You can also keep secrets in `.env` and reference them from YAML: + +```env +OPENAI_API_KEY=sk-your-key-here +``` + +```yaml +apiKeys: + openai: ${OPENAI_API_KEY} +``` + +The container applies `openclaw-os.yaml` on every start and stores API keys in OpenClaw's auth-profile store under `./data/openclaw`. + +To print the token-authenticated OpenClaw OS URL: + +```bash +./scripts/openclaw.sh os url +``` + +Windows PowerShell: + +```powershell +.\scripts\openclaw.ps1 os url +``` + +## CLI Examples + +```bash +# Check gateway health/status. +./scripts/openclaw.sh gateway probe + +# Configure models, channels, plugins, and gateway settings. +./scripts/openclaw.sh configure + +# Approve browser/device pairing requests. +./scripts/openclaw.sh devices list +./scripts/openclaw.sh devices approve +``` + +Use the wrapper scripts for gateway/device commands instead of a host-installed `openclaw` binary. The wrapper always uses the OpenClaw CLI inside this image, so its gateway protocol matches the container. A host-installed OpenClaw CLI may be older than the Docker gateway and fail with `protocol mismatch`. + +## Version Pins + +Defaults are pinned in `.env.example`: + +- `openclaw@2026.5.27` +- `@openuidev/openclaw-os-plugin@0.1.5` + +Change `OPENCLAW_VERSION` or `OPENCLAW_OS_PLUGIN_VERSION`, then rebuild: + +```bash +docker compose build --no-cache +docker compose up -d +``` + +## Persistence + +Compose bind-mounts state into: + +- `./data/openclaw` for OpenClaw config, workspace, sessions, and installed plugin state +- `./data/openclaw-auth-profile-secrets` for auth-profile secret material + +Keep both directories private. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..3921fe8 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,83 @@ +services: + openclaw-gateway: + image: ${OPENCLAW_IMAGE:-openclaw-os:local} + build: + context: . + args: + OPENCLAW_VERSION: ${OPENCLAW_VERSION:-2026.5.27} + OPENCLAW_OS_PLUGIN_VERSION: ${OPENCLAW_OS_PLUGIN_VERSION:-0.1.5} + env_file: + - path: .env + required: false + environment: + HOME: /home/node + OPENCLAW_HOME: /home/node + OPENCLAW_STATE_DIR: /home/node/.openclaw + OPENCLAW_CONFIG_DIR: /home/node/.openclaw + OPENCLAW_CONFIG_PATH: /home/node/.openclaw/openclaw.json + OPENCLAW_WORKSPACE_DIR: /home/node/.openclaw/workspace + OPENCLAW_GATEWAY_BIND: ${OPENCLAW_GATEWAY_BIND:-lan} + OPENCLAW_GATEWAY_AUTH_MODE: ${OPENCLAW_GATEWAY_AUTH_MODE:-token} + OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-} + OPENCLAW_GATEWAY_HOST_PORT: ${OPENCLAW_GATEWAY_HOST_PORT:-18789} + OPENCLAW_CONTROL_UI_EXTRA_ORIGINS: ${OPENCLAW_CONTROL_UI_EXTRA_ORIGINS:-} + OPENCLAW_BOOTSTRAP_CONFIG: /etc/openclaw-os/openclaw-os.yaml + OPENCLAW_OS_AUTO_INSTALL: ${OPENCLAW_OS_AUTO_INSTALL:-1} + OPENCLAW_OS_PLUGIN_SPEC: ${OPENCLAW_OS_PLUGIN_SPEC:-} + OPENCLAW_DISABLE_BONJOUR: ${OPENCLAW_DISABLE_BONJOUR:-1} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} + GOOGLE_API_KEY: ${GOOGLE_API_KEY:-} + GEMINI_API_KEY: ${GEMINI_API_KEY:-} + TZ: ${OPENCLAW_TZ:-UTC} + volumes: + - ./openclaw-os.yaml:/etc/openclaw-os/openclaw-os.yaml:ro + - ${OPENCLAW_CONFIG_DIR:-./data/openclaw}:/home/node/.openclaw + - ${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-./data/openclaw-auth-profile-secrets}:/home/node/.config/openclaw + ports: + - "${OPENCLAW_GATEWAY_HOST_PORT:-18789}:18789" + - "${OPENCLAW_BRIDGE_HOST_PORT:-18790}:18790" + - "${OPENCLAW_MSTEAMS_HOST_PORT:-3978}:3978" + extra_hosts: + - "host.docker.internal:host-gateway" + cap_drop: + - NET_RAW + - NET_ADMIN + security_opt: + - no-new-privileges:true + restart: unless-stopped + + openclaw-cli: + image: ${OPENCLAW_IMAGE:-openclaw-os:local} + profiles: + - tools + network_mode: "service:openclaw-gateway" + env_file: + - path: .env + required: false + environment: + HOME: /home/node + OPENCLAW_HOME: /home/node + OPENCLAW_STATE_DIR: /home/node/.openclaw + OPENCLAW_CONFIG_DIR: /home/node/.openclaw + OPENCLAW_CONFIG_PATH: /home/node/.openclaw/openclaw.json + OPENCLAW_WORKSPACE_DIR: /home/node/.openclaw/workspace + OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-} + OPENCLAW_BOOTSTRAP_CONFIG: /etc/openclaw-os/openclaw-os.yaml + BROWSER: echo + TZ: ${OPENCLAW_TZ:-UTC} + volumes: + - ./openclaw-os.yaml:/etc/openclaw-os/openclaw-os.yaml:ro + - ${OPENCLAW_CONFIG_DIR:-./data/openclaw}:/home/node/.openclaw + - ${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-./data/openclaw-auth-profile-secrets}:/home/node/.config/openclaw + cap_drop: + - NET_RAW + - NET_ADMIN + security_opt: + - no-new-privileges:true + entrypoint: ["tini", "-s", "--", "docker-entrypoint.sh"] + command: ["cli", "--help"] + depends_on: + openclaw-gateway: + condition: service_healthy diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 0000000..bdcb203 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,285 @@ +#!/usr/bin/env bash +set -euo pipefail + +is_truthy() { + case "${1:-}" in + 1 | true | TRUE | yes | YES | on | ON) return 0 ;; + *) return 1 ;; + esac +} + +ensure_dirs() { + mkdir -p \ + "${OPENCLAW_STATE_DIR}" \ + "${OPENCLAW_WORKSPACE_DIR}" \ + "$(dirname "${OPENCLAW_CONFIG_PATH}")" \ + /home/node/.config/openclaw +} + +configure_gateway() { + local batch_json + + batch_json="$(node -e ' + const bindMode = process.env.OPENCLAW_GATEWAY_BIND || "lan"; + const authMode = process.env.OPENCLAW_GATEWAY_AUTH_MODE || "token"; + const port = process.env.OPENCLAW_GATEWAY_HOST_PORT || "18789"; + const token = process.env.OPENCLAW_GATEWAY_TOKEN || ""; + const extra = (process.env.OPENCLAW_CONTROL_UI_EXTRA_ORIGINS || "") + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean); + const ops = [ + { path: "gateway.mode", value: "local" }, + { path: "gateway.bind", value: bindMode }, + { path: "gateway.auth.mode", value: authMode }, + { + path: "gateway.controlUi.allowedOrigins", + value: [`http://localhost:${port}`, `http://127.0.0.1:${port}`, ...extra], + }, + ]; + if (token) { + ops.push({ path: "gateway.auth.token", value: token }); + } + process.stdout.write(JSON.stringify(ops)); + ')" + + openclaw config set --batch-json "$batch_json" >/dev/null +} + +apply_bootstrap_config() { + local config_path="${OPENCLAW_BOOTSTRAP_CONFIG:-/etc/openclaw-os/openclaw-os.yaml}" + local patch_file="/tmp/openclaw-bootstrap.patch.json" + local keys_file="/tmp/openclaw-bootstrap-api-keys.tsv" + + if [ ! -f "$config_path" ]; then + return 0 + fi + + python3 - "$config_path" "$patch_file" "$keys_file" <<'PY' +import json +import os +import sys +import yaml + +config_path, patch_path, keys_path = sys.argv[1:4] + +with open(config_path, "r", encoding="utf-8") as f: + raw = yaml.safe_load(f) or {} + +cfg = raw.get("openclaw", raw) or {} +patch = {} +keys = [] + +PLACEHOLDERS = { + "", + "change-me", + "replace-me", + "paste-your-key-here", + "sk-...", + "sk-ant-...", + "your-api-key", +} + + +def useful(value): + if value is None: + return False + if isinstance(value, str): + return value.strip().lower() not in PLACEHOLDERS + return True + + +def resolve(value): + if not isinstance(value, str): + return value + value = value.strip() + if value.startswith("${") and value.endswith("}"): + return os.environ.get(value[2:-1], "") + return value + + +def deep_merge(target, source): + for key, value in source.items(): + if isinstance(value, dict) and isinstance(target.get(key), dict): + deep_merge(target[key], value) + else: + target[key] = value + + +gateway = cfg.get("gateway") or {} +gateway_patch = {} +if useful(gateway.get("bind")): + gateway_patch["bind"] = str(gateway["bind"]) +if useful(gateway.get("authMode")): + gateway_patch.setdefault("auth", {})["mode"] = str(gateway["authMode"]) +if useful(gateway.get("authToken")): + gateway_patch.setdefault("auth", {})["token"] = str(resolve(gateway["authToken"])) +if useful(gateway.get("allowedOrigins")): + gateway_patch.setdefault("controlUi", {})["allowedOrigins"] = list(gateway["allowedOrigins"]) +if gateway_patch: + patch.setdefault("gateway", {}) + deep_merge(patch["gateway"], gateway_patch) + +model = cfg.get("model") or {} +primary = model.get("primary") +fallbacks = model.get("fallbacks") or [] +if useful(primary): + primary = str(primary) + model_patch = {"primary": primary} + if fallbacks: + model_patch["fallbacks"] = [str(item) for item in fallbacks if useful(item)] + patch.setdefault("agents", {}).setdefault("defaults", {})["model"] = model_patch + allowed_models = patch["agents"]["defaults"].setdefault("models", {}) + allowed_models.setdefault(primary, {}) + for fallback in model_patch.get("fallbacks", []): + allowed_models.setdefault(fallback, {}) +if useful(model.get("thinking")): + patch.setdefault("agents", {}).setdefault("defaults", {})["thinkingDefault"] = str(model["thinking"]) +if useful(model.get("workspace")): + patch.setdefault("agents", {}).setdefault("defaults", {})["workspace"] = str(model["workspace"]) + +providers = cfg.get("providers") or {} +for provider_id, provider_cfg in providers.items(): + if not isinstance(provider_cfg, dict): + continue + clean = {} + for key in ("baseUrl", "api", "auth", "contextWindow", "contextTokens", "maxTokens", "timeoutSeconds"): + if useful(provider_cfg.get(key)): + clean[key] = resolve(provider_cfg[key]) + if useful(provider_cfg.get("apiKey")): + clean["apiKey"] = resolve(provider_cfg["apiKey"]) + models = provider_cfg.get("models") + if isinstance(models, list): + clean["models"] = {str(name): {} for name in models if useful(name)} + elif isinstance(models, dict): + clean["models"] = models + if clean: + patch.setdefault("models", {}).setdefault("providers", {})[str(provider_id)] = clean + +api_keys = cfg.get("apiKeys") or cfg.get("api_keys") or {} +for provider_id, api_key in api_keys.items(): + api_key = resolve(api_key) + if useful(api_key): + provider_id = str(provider_id) + profile_id = f"{provider_id}:docker" + keys.append((provider_id, profile_id, str(api_key))) + patch.setdefault("auth", {}).setdefault("profiles", {})[profile_id] = { + "provider": provider_id, + "mode": "api_key", + } + +for item in cfg.get("authProfiles") or []: + if not isinstance(item, dict): + continue + provider_id = item.get("provider") + api_key = resolve(item.get("apiKey")) + if useful(provider_id) and useful(api_key): + provider_id = str(provider_id) + profile_id = str(item.get("profileId") or f"{provider_id}:docker") + keys.append((provider_id, profile_id, str(api_key))) + patch.setdefault("auth", {}).setdefault("profiles", {})[profile_id] = { + "provider": provider_id, + "mode": "api_key", + } + +with open(patch_path, "w", encoding="utf-8") as f: + json.dump(patch, f) + +with open(keys_path, "w", encoding="utf-8") as f: + for provider_id, profile_id, api_key in keys: + f.write(f"{provider_id}\t{profile_id}\t{api_key}\n") +PY + + if [ -s "$patch_file" ] && [ "$(node -e "const fs=require('node:fs'); const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8')); process.stdout.write(Object.keys(p).length ? 'yes' : 'no')" "$patch_file")" = "yes" ]; then + openclaw config patch --stdin <"$patch_file" >/dev/null + fi + + if [ -s "$keys_file" ]; then + node - "$keys_file" "${OPENCLAW_STATE_DIR}/agents/main/agent/auth-profiles.json" <<'NODE' +const fs = require("node:fs"); +const path = require("node:path"); + +const [keysPath, storePath] = process.argv.slice(2); +let store = { version: 1, profiles: {} }; +try { + store = JSON.parse(fs.readFileSync(storePath, "utf8")); + if (!store || typeof store !== "object") store = { version: 1, profiles: {} }; + if (!store.profiles || typeof store.profiles !== "object") store.profiles = {}; +} catch { + // First run. +} + +for (const line of fs.readFileSync(keysPath, "utf8").split(/\r?\n/)) { + if (!line.trim()) continue; + const [provider, profileId, ...rest] = line.split("\t"); + const apiKey = rest.join("\t"); + if (!provider || !profileId || !apiKey) continue; + store.profiles[profileId] = { + type: "api_key", + provider, + key: apiKey, + }; +} + +fs.mkdirSync(path.dirname(storePath), { recursive: true, mode: 0o700 }); +fs.writeFileSync(storePath, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 }); +NODE + fi +} + +install_openclaw_os_plugin() { + if ! is_truthy "${OPENCLAW_OS_AUTO_INSTALL:-1}"; then + return 0 + fi + + local spec="${OPENCLAW_OS_PLUGIN_SPEC:-${OPENCLAW_OS_PLUGIN_PATH}}" + local marker="${OPENCLAW_STATE_DIR}/.openclaw-os-plugin.spec" + + if [ -f "$marker" ] && [ "$(cat "$marker")" = "$spec" ]; then + return 0 + fi + + echo "Installing OpenClaw OS plugin from ${spec}..." + openclaw plugins install "$spec" --force + printf '%s' "$spec" >"$marker" +} + +start_gateway() { + local bind_mode="${OPENCLAW_GATEWAY_BIND:-lan}" + local port="${OPENCLAW_GATEWAY_PORT:-18789}" + local auth_mode="${OPENCLAW_GATEWAY_AUTH_MODE:-token}" + + ensure_dirs + configure_gateway + apply_bootstrap_config + install_openclaw_os_plugin + + exec openclaw gateway \ + --bind "$bind_mode" \ + --port "$port" \ + --auth "$auth_mode" +} + +case "${1:-gateway}" in + gateway) + shift || true + start_gateway "$@" + ;; + configure) + ensure_dirs + configure_gateway + apply_bootstrap_config + install_openclaw_os_plugin + ;; + cli) + shift || true + exec openclaw "$@" + ;; + openclaw) + shift || true + exec openclaw "$@" + ;; + *) + exec openclaw "$@" + ;; +esac diff --git a/docker/openclaw-os.yaml b/docker/openclaw-os.yaml new file mode 100644 index 0000000..522b3fd --- /dev/null +++ b/docker/openclaw-os.yaml @@ -0,0 +1,63 @@ +# OpenClaw OS Docker bootstrap config. +# +# Edit this file before first start. The container applies it on every start. +# Keep this file private if you paste API keys directly. + +gateway: + # Keep "lan" for Docker port publishing. + bind: lan + authMode: token + + # Usually leave this blank. scripts/init-env.sh or scripts/init-env.ps1 + # generates OPENCLAW_GATEWAY_TOKEN in .env and the container persists it. + authToken: + + # Add reverse-proxy origins here, for example: + # allowedOrigins: + # - https://openclaw.example.com + +model: + # Full model id: provider/model. + primary: openai/gpt-5.5 + + # Optional fallback order. + fallbacks: [] + + # off, minimal, low, medium, high, xhigh, adaptive, or max. + thinking: medium + + # Container path exposed to the agent. + workspace: /home/node/.openclaw/workspace + +# Simple API-key setup. Values may be literal keys or env references such as ${OPENAI_API_KEY}. +# Blank values are ignored. +apiKeys: + openai: ${OPENAI_API_KEY} + anthropic: + openrouter: + google: + +# Optional custom/self-hosted providers. +# Use host.docker.internal when the model server runs on the host machine. +providers: + # ollama: + # baseUrl: http://host.docker.internal:11434/v1 + # api: openai-completions + # auth: api-key + # apiKey: ollama + # models: + # - llama3.1 + # + # openrouter: + # baseUrl: https://openrouter.ai/api/v1 + # api: openai-completions + # auth: api-key + # apiKey: ${OPENROUTER_API_KEY} + # models: + # - openai/gpt-4.1 + +# Advanced: define explicit auth profile ids. +authProfiles: [] + # - provider: openai + # profileId: openai:docker + # apiKey: ${OPENAI_API_KEY} diff --git a/docker/scripts/init-env.ps1 b/docker/scripts/init-env.ps1 new file mode 100644 index 0000000..ec3856e --- /dev/null +++ b/docker/scripts/init-env.ps1 @@ -0,0 +1,30 @@ +$ErrorActionPreference = "Stop" + +$RootDir = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$EnvFile = Join-Path $RootDir ".env" +$ExampleFile = Join-Path $RootDir ".env.example" + +if (!(Test-Path $EnvFile)) { + Copy-Item $ExampleFile $EnvFile +} + +$envText = Get-Content $EnvFile -Raw +if ($envText -notmatch "(?m)^OPENCLAW_GATEWAY_TOKEN=.+") { + $bytes = New-Object byte[] 32 + [Security.Cryptography.RandomNumberGenerator]::Fill($bytes) + $token = -join ($bytes | ForEach-Object { $_.ToString("x2") }) + + if ($envText -match "(?m)^OPENCLAW_GATEWAY_TOKEN=") { + $envText = $envText -replace "(?m)^OPENCLAW_GATEWAY_TOKEN=.*$", "OPENCLAW_GATEWAY_TOKEN=$token" + } else { + $envText = $envText.TrimEnd() + "`nOPENCLAW_GATEWAY_TOKEN=$token`n" + } + Set-Content -Path $EnvFile -Value $envText -NoNewline +} + +New-Item -ItemType Directory -Force -Path (Join-Path $RootDir "data/openclaw") | Out-Null +New-Item -ItemType Directory -Force -Path (Join-Path $RootDir "data/openclaw-auth-profile-secrets") | Out-Null + +Write-Host "Initialized $EnvFile" +Write-Host "Gateway token:" +(Select-String -Path $EnvFile -Pattern "^OPENCLAW_GATEWAY_TOKEN=(.*)$").Matches[0].Groups[1].Value diff --git a/docker/scripts/init-env.sh b/docker/scripts/init-env.sh new file mode 100755 index 0000000..e41a39c --- /dev/null +++ b/docker/scripts/init-env.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="${ROOT_DIR}/.env" + +if [ ! -f "${ENV_FILE}" ]; then + cp "${ROOT_DIR}/.env.example" "${ENV_FILE}" +fi + +if ! grep -q '^OPENCLAW_GATEWAY_TOKEN=.\+' "${ENV_FILE}"; then + token="$(openssl rand -hex 32)" + tmp_file="$(mktemp)" + awk -v token="${token}" ' + BEGIN { replaced = 0 } + /^OPENCLAW_GATEWAY_TOKEN=/ { + print "OPENCLAW_GATEWAY_TOKEN=" token + replaced = 1 + next + } + { print } + END { + if (!replaced) { + print "OPENCLAW_GATEWAY_TOKEN=" token + } + } + ' "${ENV_FILE}" >"${tmp_file}" + mv "${tmp_file}" "${ENV_FILE}" +fi + +mkdir -p "${ROOT_DIR}/data/openclaw" "${ROOT_DIR}/data/openclaw-auth-profile-secrets" + +echo "Initialized ${ENV_FILE}" +echo "Gateway token:" +grep '^OPENCLAW_GATEWAY_TOKEN=' "${ENV_FILE}" | sed 's/^OPENCLAW_GATEWAY_TOKEN=//' diff --git a/docker/scripts/openclaw.ps1 b/docker/scripts/openclaw.ps1 new file mode 100644 index 0000000..c130864 --- /dev/null +++ b/docker/scripts/openclaw.ps1 @@ -0,0 +1,8 @@ +$ErrorActionPreference = "Stop" + +$RootDir = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) + +docker compose ` + --project-directory $RootDir ` + -f (Join-Path $RootDir "docker-compose.yml") ` + run --rm openclaw-cli @args diff --git a/docker/scripts/openclaw.sh b/docker/scripts/openclaw.sh new file mode 100755 index 0000000..39d914c --- /dev/null +++ b/docker/scripts/openclaw.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +exec docker compose \ + --project-directory "${ROOT_DIR}" \ + -f "${ROOT_DIR}/docker-compose.yml" \ + run --rm openclaw-cli "$@"