Merge d927a4e55f20a6d3bc505c23c66c85cb2bbda370 into 9fb78453e088cd7b553d7779faa0de5c83708e70
This commit is contained in:
commit
4d09148687
126
Dockerfile
126
Dockerfile
@ -3,8 +3,9 @@
|
||||
# Opt-in extension dependencies at build time (space-separated directory names).
|
||||
# Example: docker build --build-arg OPENCLAW_EXTENSIONS="diagnostics-otel matrix" .
|
||||
#
|
||||
# Multi-stage build produces a minimal runtime image without build tools,
|
||||
# source code, or Bun. Works with Docker, Buildx, and Podman.
|
||||
# Multi-stage build keeps source code and Bun out of the runtime image while
|
||||
# still allowing optional runtime tooling for Docker-hosted workflows.
|
||||
# Works with Docker, Buildx, and Podman.
|
||||
# The ext-deps stage extracts only the package.json files we need from
|
||||
# extensions/, so the main build layer is not invalidated by unrelated
|
||||
# extension source changes.
|
||||
@ -55,6 +56,13 @@ ENV PATH="/root/.bun/bin:${PATH}"
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
ENV PNPM_HOME=/home/node/.local/share/pnpm
|
||||
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
|
||||
ENV GOPATH=/home/node/go
|
||||
ENV PATH="/usr/local/go/bin:${PNPM_HOME}:${NPM_CONFIG_PREFIX}/bin:${GOPATH}/bin:${PATH}"
|
||||
RUN mkdir -p "${PNPM_HOME}" "${NPM_CONFIG_PREFIX}/bin" "${GOPATH}/bin" && \
|
||||
chown -R node:node /home/node/.local /home/node/.npm-global /home/node/go
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
||||
@ -126,17 +134,19 @@ LABEL org.opencontainers.image.source="https://github.com/openclaw/openclaw" \
|
||||
org.opencontainers.image.description="OpenClaw gateway and CLI runtime container image"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system utilities present in bookworm but missing in bookworm-slim.
|
||||
# On the full bookworm image these are already installed (apt-get is a no-op).
|
||||
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
procps hostname curl git lsof openssl
|
||||
|
||||
ENV PNPM_HOME=/home/node/.local/share/pnpm
|
||||
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
|
||||
ENV COREPACK_HOME=/usr/local/share/corepack
|
||||
ENV GOPATH=/home/node/go
|
||||
ENV HOMEBREW_PREFIX=/home/linuxbrew/.linuxbrew
|
||||
ENV HOMEBREW_CELLAR=/home/linuxbrew/.linuxbrew/Cellar
|
||||
ENV HOMEBREW_REPOSITORY=/home/linuxbrew/.linuxbrew/Homebrew
|
||||
ENV PATH="/usr/local/go/bin:${PNPM_HOME}:${NPM_CONFIG_PREFIX}/bin:${GOPATH}/bin:${HOMEBREW_PREFIX}/bin:${HOMEBREW_PREFIX}/sbin:${PATH}"
|
||||
RUN chown node:node /app
|
||||
RUN mkdir -p /home/node/.cache "${PNPM_HOME}" "${NPM_CONFIG_PREFIX}/bin" "${COREPACK_HOME}" \
|
||||
"${GOPATH}/bin" "${HOMEBREW_REPOSITORY}" "${HOMEBREW_CELLAR}" "${HOMEBREW_PREFIX}/bin" && \
|
||||
chown -R node:node /home/node/.cache /home/node/.local /home/node/.npm-global /home/node/go /home/linuxbrew "${COREPACK_HOME}"
|
||||
RUN corepack enable
|
||||
|
||||
COPY --from=runtime-assets --chown=node:node /app/dist ./dist
|
||||
COPY --from=runtime-assets --chown=node:node /app/node_modules ./node_modules
|
||||
@ -167,15 +177,28 @@ RUN install -d -m 0755 "$COREPACK_HOME" && \
|
||||
done && \
|
||||
chmod -R a+rX "$COREPACK_HOME"
|
||||
|
||||
# Install additional system packages needed by your skills or extensions.
|
||||
# Example: docker build --build-arg OPENCLAW_DOCKER_APT_PACKAGES="python3 wget" .
|
||||
# Install baseline system packages needed by the slim runtime and common
|
||||
# Docker workflows. Extra packages can still be layered in via
|
||||
# OPENCLAW_DOCKER_APT_PACKAGES without reinstalling duplicates.
|
||||
ARG OPENCLAW_DOCKER_APT_PACKAGES=""
|
||||
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
if [ -n "$OPENCLAW_DOCKER_APT_PACKAGES" ]; then \
|
||||
apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $OPENCLAW_DOCKER_APT_PACKAGES; \
|
||||
fi
|
||||
set -eux; \
|
||||
BASE_APT_PACKAGES="\
|
||||
cron gosu \
|
||||
git curl wget ca-certificates jq unzip ripgrep procps hostname openssl lsof file \
|
||||
python3 python3-pip python3-venv \
|
||||
xvfb xauth \
|
||||
libgbm1 libnss3 libasound2 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libxss1 libgtk-3-0"; \
|
||||
EXTRA_APT_PACKAGES=""; \
|
||||
for pkg in $OPENCLAW_DOCKER_APT_PACKAGES; do \
|
||||
case " ${BASE_APT_PACKAGES} " in \
|
||||
*" ${pkg} "*) ;; \
|
||||
*) EXTRA_APT_PACKAGES="${EXTRA_APT_PACKAGES} ${pkg}" ;; \
|
||||
esac; \
|
||||
done; \
|
||||
apt-get update; \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ${BASE_APT_PACKAGES} ${EXTRA_APT_PACKAGES}
|
||||
|
||||
# Optionally install Chromium and Xvfb for browser automation.
|
||||
# Build with: docker build --build-arg OPENCLAW_INSTALL_BROWSER=1 ...
|
||||
@ -185,14 +208,67 @@ ARG OPENCLAW_INSTALL_BROWSER=""
|
||||
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
if [ -n "$OPENCLAW_INSTALL_BROWSER" ]; then \
|
||||
apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && \
|
||||
mkdir -p /home/node/.cache/ms-playwright && \
|
||||
PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright \
|
||||
node /app/node_modules/playwright-core/cli.js install --with-deps chromium && \
|
||||
chown -R node:node /home/node/.cache/ms-playwright; \
|
||||
fi
|
||||
|
||||
# ---- Install Go (official) ----
|
||||
# Fetch the latest stable version from go.dev and install the correct arch.
|
||||
RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; \
|
||||
case "$arch" in \
|
||||
amd64) GOARCH=amd64 ;; \
|
||||
arm64) GOARCH=arm64 ;; \
|
||||
*) echo "Unsupported arch: $arch" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
GOVERSION="$(curl -fsSL 'https://go.dev/dl/?mode=json' | jq -r 'map(select(.stable==true)) | .[0].version')" ; \
|
||||
echo "Installing Go ${GOVERSION} for linux-${GOARCH}"; \
|
||||
curl -fsSL "https://go.dev/dl/${GOVERSION}.linux-${GOARCH}.tar.gz" -o /tmp/go.tgz; \
|
||||
rm -rf /usr/local/go; \
|
||||
tar -C /usr/local -xzf /tmp/go.tgz; \
|
||||
rm -f /tmp/go.tgz; \
|
||||
/usr/local/go/bin/go version
|
||||
|
||||
# Ensure Go is first in PATH (no old go ahead of it)
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
|
||||
# ---- Install gog (gogcli) ----
|
||||
# Pin version by setting GOGCLI_TAG at build time.
|
||||
# Default stays pinned for reproducible CI builds.
|
||||
ARG GOGCLI_TAG=v0.11.0
|
||||
RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; \
|
||||
case "$arch" in \
|
||||
amd64) GOGARCH=amd64 ;; \
|
||||
arm64) GOGARCH=arm64 ;; \
|
||||
*) echo "Unsupported arch: $arch" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
tag="$GOGCLI_TAG"; \
|
||||
if [ "$tag" = "latest" ]; then \
|
||||
tag="$(curl -fsSI -H 'User-Agent: openclaw-docker-build' https://github.com/steipete/gogcli/releases/latest | awk 'tolower($1)==\"location:\" {print $2}' | tr -d '\r' | awk -F/ '{print $NF}' | tail -n1)"; \
|
||||
if [ -z "$tag" ]; then \
|
||||
echo "WARN: Failed to resolve gogcli latest release tag; falling back to v0.11.0" >&2; \
|
||||
tag="v0.11.0"; \
|
||||
fi; \
|
||||
fi; \
|
||||
ver="${tag#v}"; \
|
||||
url="https://github.com/steipete/gogcli/releases/download/$tag/gogcli_${ver}_linux_${GOGARCH}.tar.gz"; \
|
||||
echo "Downloading: $url"; \
|
||||
curl -fsSL "$url" -o /tmp/gogcli.tgz; \
|
||||
tar -xzf /tmp/gogcli.tgz -C /tmp; \
|
||||
install -m 0755 /tmp/gog /usr/local/bin/gog; \
|
||||
rm -f /tmp/gog /tmp/gogcli.tgz; \
|
||||
gog --help >/dev/null
|
||||
|
||||
# Install Linuxbrew in a node-writable prefix so brew installs work at runtime.
|
||||
RUN set -eux; \
|
||||
curl -fsSL https://github.com/Homebrew/brew/tarball/master | tar xz --strip-components=1 -C "${HOMEBREW_REPOSITORY}"; \
|
||||
ln -sf ../Homebrew/bin/brew "${HOMEBREW_PREFIX}/bin/brew"; \
|
||||
chown -R node:node /home/linuxbrew
|
||||
RUN gosu node brew --version >/dev/null
|
||||
|
||||
# Optionally install Docker CLI for sandbox container management.
|
||||
# Build with: docker build --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1 ...
|
||||
# Adds ~50MB. Only the CLI is installed — no Docker daemon.
|
||||
@ -225,6 +301,16 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar
|
||||
docker-ce-cli docker-compose-plugin; \
|
||||
fi
|
||||
|
||||
COPY --from=build --chown=node:node /app/scripts/docker ./scripts/docker
|
||||
# Normalize extension paths so plugin safety checks do not reject
|
||||
# world-writable directories inherited from source file modes.
|
||||
RUN for dir in /app/extensions /app/.agent /app/.agents; do \
|
||||
if [ -d "$dir" ]; then \
|
||||
find "$dir" -type d -exec chmod 755 {} +; \
|
||||
find "$dir" -type f -exec chmod 644 {} +; \
|
||||
fi; \
|
||||
done
|
||||
RUN chmod +x scripts/docker/gateway-entrypoint.sh
|
||||
# Expose the CLI binary without requiring npm global writes as non-root.
|
||||
RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \
|
||||
&& chmod 755 /app/openclaw.mjs
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
services:
|
||||
openclaw-gateway:
|
||||
image: ${OPENCLAW_IMAGE:-openclaw:local}
|
||||
user: root
|
||||
entrypoint: ["/app/scripts/docker/gateway-entrypoint.sh"]
|
||||
environment:
|
||||
HOME: /home/node
|
||||
TERM: xterm-256color
|
||||
@ -11,6 +13,7 @@ services:
|
||||
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
|
||||
TZ: ${OPENCLAW_TZ:-UTC}
|
||||
volumes:
|
||||
- ./.env:/app/.env:ro
|
||||
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
|
||||
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
|
||||
## Uncomment the lines below to enable sandbox isolation
|
||||
@ -68,6 +71,7 @@ services:
|
||||
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
|
||||
TZ: ${OPENCLAW_TZ:-UTC}
|
||||
volumes:
|
||||
- ./.env:/app/.env:ro
|
||||
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
|
||||
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
|
||||
stdin_open: true
|
||||
|
||||
0
docker-setup.sh
Executable file → Normal file
0
docker-setup.sh
Executable file → Normal file
@ -50,7 +50,6 @@ Docker is **optional**. Use it only if you want a containerized gateway or to va
|
||||
|
||||
<Step title="Complete onboarding">
|
||||
The setup script runs onboarding automatically. It will:
|
||||
|
||||
- prompt for provider API keys
|
||||
- generate a gateway token and write it to `.env`
|
||||
- start the gateway via Docker Compose
|
||||
@ -108,15 +107,121 @@ include it with `-f docker-compose.yml -f docker-compose.extra.yml`.
|
||||
|
||||
The setup script accepts these optional environment variables:
|
||||
|
||||
| Variable | Purpose |
|
||||
| ------------------------------ | ---------------------------------------------------------------- |
|
||||
| `OPENCLAW_IMAGE` | Use a remote image instead of building locally |
|
||||
| `OPENCLAW_DOCKER_APT_PACKAGES` | Install extra apt packages during build (space-separated) |
|
||||
| `OPENCLAW_EXTENSIONS` | Pre-install extension deps at build time (space-separated names) |
|
||||
| `OPENCLAW_EXTRA_MOUNTS` | Extra host bind mounts (comma-separated `source:target[:opts]`) |
|
||||
| `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume |
|
||||
| `OPENCLAW_SANDBOX` | Opt in to sandbox bootstrap (`1`, `true`, `yes`, `on`) |
|
||||
| `OPENCLAW_DOCKER_SOCKET` | Override Docker socket path |
|
||||
| Variable | Purpose |
|
||||
| ------------------------------------ | ---------------------------------------------------------------- |
|
||||
| `OPENCLAW_IMAGE` | Use a remote image instead of building locally |
|
||||
| `OPENCLAW_DOCKER_APT_PACKAGES` | Install extra apt packages during build (space-separated) |
|
||||
| `OPENCLAW_INSTALL_BROWSER` | Install Chromium and Playwright deps during the image build |
|
||||
| `OPENCLAW_EXTENSIONS` | Pre-install extension deps at build time (space-separated names) |
|
||||
| `OPENCLAW_EXTRA_MOUNTS` | Extra host bind mounts (comma-separated `source:target[:opts]`) |
|
||||
| `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume |
|
||||
| `OPENCLAW_SANDBOX` | Opt in to sandbox bootstrap (`1`, `true`, `yes`, `on`) |
|
||||
| `OPENCLAW_DOCKER_SOCKET` | Override Docker socket path |
|
||||
| `OPENCLAW_INSTALL_DOCKER_CLI` | Install Docker CLI in the image (auto-set for sandbox bootstrap) |
|
||||
| `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS` | Allow trusted private-network `ws://` client targets |
|
||||
| `OPENCLAW_TZ` | Set container timezone (for example `America/Los_Angeles`) |
|
||||
|
||||
### Use a remote image (skip local build)
|
||||
|
||||
Use image name `ghcr.io/openclaw/openclaw` (not similarly named Docker Hub
|
||||
images).
|
||||
|
||||
Common tags:
|
||||
|
||||
- `main` — latest build from `main`
|
||||
- `<version>` — release tag builds (for example `2026.3.12`)
|
||||
- `latest` — latest stable release tag
|
||||
|
||||
```bash
|
||||
export OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:latest"
|
||||
./scripts/docker/setup.sh
|
||||
```
|
||||
|
||||
### Extra mounts (optional)
|
||||
|
||||
Set `OPENCLAW_EXTRA_MOUNTS` to add host directories to both containers:
|
||||
|
||||
```bash
|
||||
export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/home/node/github:rw"
|
||||
./scripts/docker/setup.sh
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Paths must be shared with Docker Desktop on macOS/Windows.
|
||||
- Each entry must be `source:target[:options]` with no spaces or control characters.
|
||||
- Rerun the setup script after changing the value so `docker-compose.extra.yml`
|
||||
is regenerated.
|
||||
|
||||
### Persist the entire container home (optional)
|
||||
|
||||
Use `OPENCLAW_HOME_VOLUME` if you want `/home/node` to survive container
|
||||
recreation:
|
||||
|
||||
```bash
|
||||
export OPENCLAW_HOME_VOLUME="openclaw_home"
|
||||
./scripts/docker/setup.sh
|
||||
```
|
||||
|
||||
### Install extra apt packages (optional)
|
||||
|
||||
`OPENCLAW_DOCKER_APT_PACKAGES` installs additional Debian packages at build time:
|
||||
|
||||
```bash
|
||||
export OPENCLAW_DOCKER_APT_PACKAGES="ffmpeg build-essential"
|
||||
./scripts/docker/setup.sh
|
||||
```
|
||||
|
||||
The Docker image already includes a baseline for common container workflows:
|
||||
`cron`, `gosu`, `pnpm`, non-root `npm -g`, `go install`, Linuxbrew, and browser
|
||||
runtime libraries. Add only the extra packages your environment needs.
|
||||
|
||||
### Install browser dependencies at build time (optional)
|
||||
|
||||
If you want Chromium baked into the image instead of downloading it later:
|
||||
|
||||
```bash
|
||||
export OPENCLAW_INSTALL_BROWSER=1
|
||||
./scripts/docker/setup.sh
|
||||
```
|
||||
|
||||
This installs Chromium through Playwright during the image build and stores it
|
||||
under `/home/node/.cache/ms-playwright`.
|
||||
|
||||
### Pre-install extension dependencies (optional)
|
||||
|
||||
Extensions with their own `package.json` can be baked into the image:
|
||||
|
||||
```bash
|
||||
export OPENCLAW_EXTENSIONS="diagnostics-otel matrix"
|
||||
./scripts/docker/setup.sh
|
||||
```
|
||||
|
||||
### Keep macOS awake (optional)
|
||||
|
||||
Docker itself does not reliably keep macOS awake. If the gateway must stay
|
||||
online for Slack, webhooks, or scheduled jobs:
|
||||
|
||||
```bash
|
||||
scripts/openclaw-keepawake.sh on
|
||||
scripts/openclaw-keepawake.sh status
|
||||
scripts/openclaw-keepawake.sh off
|
||||
```
|
||||
|
||||
By default this uses `caffeinate -imsu`, which allows the displays to sleep.
|
||||
Set `OPENCLAW_KEEPAWAKE_FLAGS=-dimsu` if you also want to keep the displays on.
|
||||
|
||||
### Base image metadata
|
||||
|
||||
The main Docker image uses `node:24-bookworm` and publishes OCI base-image
|
||||
annotations including:
|
||||
|
||||
- `org.opencontainers.image.base.name`
|
||||
- `org.opencontainers.image.base.digest`
|
||||
- `org.opencontainers.image.source`
|
||||
- `org.opencontainers.image.documentation`
|
||||
|
||||
Reference: [OCI image annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
||||
|
||||
### Health checks
|
||||
|
||||
@ -279,6 +384,89 @@ See the [`ClawDock` Helper README](https://github.com/openclaw/openclaw/blob/mai
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Backup and migration (Intel Mac to Apple Silicon)
|
||||
|
||||
For low-disruption host migration, move OpenClaw data and config, then rebuild
|
||||
the Docker image natively on the new machine.
|
||||
|
||||
Use:
|
||||
|
||||
- `scripts/migrate/backup-openclaw.sh` on the source host
|
||||
- `scripts/migrate/restore-openclaw.sh` on the target host
|
||||
|
||||
### 1) Create a backup on the source host
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
scripts/migrate/backup-openclaw.sh
|
||||
```
|
||||
|
||||
The archive includes:
|
||||
|
||||
- OpenClaw config dir (`OPENCLAW_CONFIG_DIR` or `~/.openclaw`)
|
||||
- OpenClaw workspace dir (`OPENCLAW_WORKSPACE_DIR` or `~/.openclaw/workspace`)
|
||||
- `.env` and Docker setup files from the repo root
|
||||
- metadata and an internal checksum manifest
|
||||
|
||||
Output files:
|
||||
|
||||
- `backups/openclaw-backup-<timestamp>.tar.gz`
|
||||
- `backups/openclaw-backup-<timestamp>.tar.gz.sha256`
|
||||
|
||||
Optional path overrides:
|
||||
|
||||
```bash
|
||||
scripts/migrate/backup-openclaw.sh \
|
||||
--config-dir "$HOME/.openclaw" \
|
||||
--workspace-dir "$HOME/.openclaw/workspace" \
|
||||
--output-dir "$HOME/openclaw-backups"
|
||||
```
|
||||
|
||||
### 2) Transfer the archive to the target host
|
||||
|
||||
Copy the archive and checksum file to the new machine using your normal secure
|
||||
transfer method.
|
||||
|
||||
### 3) Restore on the target host
|
||||
|
||||
From the repo root on the target host:
|
||||
|
||||
```bash
|
||||
scripts/migrate/restore-openclaw.sh --archive /path/to/openclaw-backup-<timestamp>.tar.gz
|
||||
```
|
||||
|
||||
Default restore behavior:
|
||||
|
||||
- verifies archive checksums
|
||||
- stops `openclaw-gateway` before restore
|
||||
- snapshots current config and workspace as `.pre-restore-<timestamp>`
|
||||
- restores config and workspace from backup
|
||||
- writes the backup env file as `.env.from-backup` for review
|
||||
|
||||
To overwrite `.env` directly:
|
||||
|
||||
```bash
|
||||
scripts/migrate/restore-openclaw.sh \
|
||||
--archive /path/to/openclaw-backup-<timestamp>.tar.gz \
|
||||
--apply-env
|
||||
```
|
||||
|
||||
### 4) Rebuild and validate on the target architecture
|
||||
|
||||
Always rebuild on Apple Silicon:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build --force-recreate openclaw-gateway
|
||||
docker compose run --rm openclaw-cli health
|
||||
docker compose run --rm openclaw-cli channels status --probe
|
||||
```
|
||||
|
||||
### Architecture migration note
|
||||
|
||||
Do not carry over architecture-specific binary caches from x86 to arm hosts.
|
||||
Rebuild containers and reinstall native toolchains on the target host.
|
||||
|
||||
### Running on a VPS?
|
||||
|
||||
See [Hetzner (Docker VPS)](/install/hetzner) and
|
||||
|
||||
32
scripts/docker/gateway-entrypoint.sh
Normal file
32
scripts/docker/gateway-entrypoint.sh
Normal file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "gateway-entrypoint requires root (set docker compose service user: root)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${OPENCLAW_ENABLE_CRON:-1}" = "1" ] && command -v cron >/dev/null 2>&1; then
|
||||
# Start cron daemon once; jobs are configured by user crontabs via `crontab`.
|
||||
if ! pgrep -x cron >/dev/null 2>&1; then
|
||||
cron
|
||||
fi
|
||||
fi
|
||||
|
||||
ensure_node_writable_dir() {
|
||||
local dir="$1"
|
||||
mkdir -p "$dir"
|
||||
chown -R node:node "$dir"
|
||||
}
|
||||
|
||||
export COREPACK_HOME="${COREPACK_HOME:-/home/node/.cache/node/corepack}"
|
||||
ensure_node_writable_dir /home/node/.cache
|
||||
ensure_node_writable_dir "${PNPM_HOME:-/home/node/.local/share/pnpm}"
|
||||
ensure_node_writable_dir "${NPM_CONFIG_PREFIX:-/home/node/.npm-global}"
|
||||
ensure_node_writable_dir "${GOPATH:-/home/node/go}"
|
||||
|
||||
if command -v gosu >/dev/null 2>&1; then
|
||||
exec gosu node "$@"
|
||||
fi
|
||||
|
||||
exec su -s /bin/sh node -c "$*"
|
||||
@ -235,6 +235,7 @@ 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_INSTALL_BROWSER="${OPENCLAW_INSTALL_BROWSER:-}"
|
||||
export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}"
|
||||
export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS"
|
||||
export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME"
|
||||
@ -427,6 +428,7 @@ upsert_env "$ENV_FILE" \
|
||||
DOCKER_GID \
|
||||
OPENCLAW_INSTALL_DOCKER_CLI \
|
||||
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS \
|
||||
OPENCLAW_INSTALL_BROWSER \
|
||||
OPENCLAW_TZ
|
||||
|
||||
if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then
|
||||
@ -435,6 +437,7 @@ if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then
|
||||
--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:-}" \
|
||||
--build-arg "OPENCLAW_INSTALL_BROWSER=${OPENCLAW_INSTALL_BROWSER}" \
|
||||
-t "$IMAGE_NAME" \
|
||||
-f "$ROOT_DIR/Dockerfile" \
|
||||
"$ROOT_DIR"
|
||||
|
||||
208
scripts/migrate/backup-openclaw.sh
Executable file
208
scripts/migrate/backup-openclaw.sh
Executable file
@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/migrate/backup-openclaw.sh [options]
|
||||
|
||||
Options:
|
||||
--repo-root <path> OpenClaw repo root (default: current repo)
|
||||
--env-file <path> Env file to include (default: <repo-root>/.env)
|
||||
--config-dir <path> OpenClaw config dir (default: env or ~/.openclaw)
|
||||
--workspace-dir <path> OpenClaw workspace dir (default: env or ~/.openclaw/workspace)
|
||||
--output-dir <path> Output directory for backup archive (default: <repo-root>/backups)
|
||||
--name <name> Backup name prefix (default: openclaw-backup-<timestamp>)
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1"
|
||||
}
|
||||
|
||||
strip_quotes() {
|
||||
local value="$1"
|
||||
if [[ "${value}" == \"*\" && "${value}" == *\" ]]; then
|
||||
value="${value:1:${#value}-2}"
|
||||
elif [[ "${value}" == \'*\' && "${value}" == *\' ]]; then
|
||||
value="${value:1:${#value}-2}"
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
env_value_from_file() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
[[ -f "$file" ]] || return 0
|
||||
local line
|
||||
line="$(grep -E "^(export[[:space:]]+)?${key}=" "$file" | tail -n 1 || true)"
|
||||
[[ -n "$line" ]] || return 0
|
||||
line="${line#export }"
|
||||
local value="${line#*=}"
|
||||
strip_quotes "$value"
|
||||
}
|
||||
|
||||
resolve_abs_path() {
|
||||
local p="$1"
|
||||
python3 - "$p" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
print(os.path.abspath(os.path.expanduser(path)))
|
||||
PY
|
||||
}
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
REPO_ROOT="$ROOT_DIR"
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
CONFIG_DIR=""
|
||||
WORKSPACE_DIR=""
|
||||
OUTPUT_DIR="$ROOT_DIR/backups"
|
||||
BACKUP_NAME=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repo-root)
|
||||
REPO_ROOT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--env-file)
|
||||
ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--config-dir)
|
||||
CONFIG_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--workspace-dir)
|
||||
WORKSPACE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output-dir)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--name)
|
||||
BACKUP_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail "Unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_cmd tar
|
||||
require_cmd rsync
|
||||
require_cmd shasum
|
||||
require_cmd python3
|
||||
require_cmd date
|
||||
require_cmd uname
|
||||
|
||||
REPO_ROOT="$(resolve_abs_path "$REPO_ROOT")"
|
||||
ENV_FILE="$(resolve_abs_path "$ENV_FILE")"
|
||||
OUTPUT_DIR="$(resolve_abs_path "$OUTPUT_DIR")"
|
||||
|
||||
if [[ -z "$CONFIG_DIR" ]]; then
|
||||
CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$(env_value_from_file "$ENV_FILE" OPENCLAW_CONFIG_DIR)}"
|
||||
fi
|
||||
if [[ -z "$WORKSPACE_DIR" ]]; then
|
||||
WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$(env_value_from_file "$ENV_FILE" OPENCLAW_WORKSPACE_DIR)}"
|
||||
fi
|
||||
|
||||
CONFIG_DIR="${CONFIG_DIR:-$HOME/.openclaw}"
|
||||
WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
|
||||
CONFIG_DIR="$(resolve_abs_path "$CONFIG_DIR")"
|
||||
WORKSPACE_DIR="$(resolve_abs_path "$WORKSPACE_DIR")"
|
||||
|
||||
[[ -d "$CONFIG_DIR" ]] || fail "Config directory does not exist: $CONFIG_DIR"
|
||||
[[ -d "$WORKSPACE_DIR" ]] || fail "Workspace directory does not exist: $WORKSPACE_DIR"
|
||||
[[ -d "$REPO_ROOT" ]] || fail "Repo root does not exist: $REPO_ROOT"
|
||||
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
BACKUP_NAME="${BACKUP_NAME:-openclaw-backup-${timestamp}}"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
stage="$tmpdir/stage"
|
||||
mkdir -p "$stage/payload/config" "$stage/payload/workspace" "$stage/payload/repo" "$stage/meta"
|
||||
|
||||
echo "==> Copying config directory"
|
||||
rsync -a "$CONFIG_DIR/" "$stage/payload/config/"
|
||||
|
||||
echo "==> Copying workspace directory"
|
||||
rsync -a "$WORKSPACE_DIR/" "$stage/payload/workspace/"
|
||||
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
echo "==> Including env file: $ENV_FILE"
|
||||
cp "$ENV_FILE" "$stage/payload/repo/.env"
|
||||
fi
|
||||
|
||||
for file in docker-compose.yml docker-compose.extra.yml Dockerfile docker-setup.sh; do
|
||||
if [[ -f "$REPO_ROOT/$file" ]]; then
|
||||
cp "$REPO_ROOT/$file" "$stage/payload/repo/$file"
|
||||
fi
|
||||
done
|
||||
|
||||
{
|
||||
echo "timestamp_utc=$timestamp"
|
||||
echo "source_host=$(hostname -s || hostname)"
|
||||
echo "source_arch=$(uname -m)"
|
||||
echo "source_os=$(uname -s)"
|
||||
echo "repo_root=$REPO_ROOT"
|
||||
echo "config_dir=$CONFIG_DIR"
|
||||
echo "workspace_dir=$WORKSPACE_DIR"
|
||||
} >"$stage/meta/backup.env"
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
{
|
||||
echo "# docker version"
|
||||
docker version --format '{{.Server.Version}}' 2>/dev/null || true
|
||||
echo
|
||||
echo "# docker compose ps"
|
||||
docker compose -f "$REPO_ROOT/docker-compose.yml" ps 2>/dev/null || true
|
||||
} >"$stage/meta/docker.txt"
|
||||
fi
|
||||
|
||||
if command -v git >/dev/null 2>&1 && [[ -d "$REPO_ROOT/.git" ]]; then
|
||||
{
|
||||
echo "branch=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
|
||||
echo "commit=$(git -C "$REPO_ROOT" rev-parse HEAD)"
|
||||
echo
|
||||
echo "# status"
|
||||
git -C "$REPO_ROOT" status --short
|
||||
} >"$stage/meta/git.txt"
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$stage"
|
||||
find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 shasum -a 256 > SHA256SUMS
|
||||
)
|
||||
|
||||
archive_path="$OUTPUT_DIR/${BACKUP_NAME}.tar.gz"
|
||||
(
|
||||
cd "$stage"
|
||||
tar -czf "$archive_path" .
|
||||
)
|
||||
shasum -a 256 "$archive_path" > "${archive_path}.sha256"
|
||||
|
||||
echo
|
||||
echo "Backup created:"
|
||||
echo " $archive_path"
|
||||
echo " ${archive_path}.sha256"
|
||||
echo
|
||||
echo "Next step on target host:"
|
||||
echo " scripts/migrate/restore-openclaw.sh --archive \"$archive_path\""
|
||||
201
scripts/migrate/restore-openclaw.sh
Executable file
201
scripts/migrate/restore-openclaw.sh
Executable file
@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/migrate/restore-openclaw.sh --archive <path> [options]
|
||||
|
||||
Options:
|
||||
--archive <path> Backup archive created by backup-openclaw.sh (required)
|
||||
--repo-root <path> OpenClaw repo root (default: current repo)
|
||||
--env-file <path> Env file path (default: <repo-root>/.env)
|
||||
--config-dir <path> OpenClaw config dir (default: env or ~/.openclaw)
|
||||
--workspace-dir <path> OpenClaw workspace dir (default: env or ~/.openclaw/workspace)
|
||||
--apply-env Overwrite --env-file with backup .env (default: false)
|
||||
--no-stop Do not stop gateway container before restore
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1"
|
||||
}
|
||||
|
||||
strip_quotes() {
|
||||
local value="$1"
|
||||
if [[ "${value}" == \"*\" && "${value}" == *\" ]]; then
|
||||
value="${value:1:${#value}-2}"
|
||||
elif [[ "${value}" == \'*\' && "${value}" == *\' ]]; then
|
||||
value="${value:1:${#value}-2}"
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
env_value_from_file() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
[[ -f "$file" ]] || return 0
|
||||
local line
|
||||
line="$(grep -E "^(export[[:space:]]+)?${key}=" "$file" | tail -n 1 || true)"
|
||||
[[ -n "$line" ]] || return 0
|
||||
line="${line#export }"
|
||||
local value="${line#*=}"
|
||||
strip_quotes "$value"
|
||||
}
|
||||
|
||||
resolve_abs_path() {
|
||||
local p="$1"
|
||||
python3 - "$p" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
print(os.path.abspath(os.path.expanduser(path)))
|
||||
PY
|
||||
}
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
REPO_ROOT="$ROOT_DIR"
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
ARCHIVE_PATH=""
|
||||
CONFIG_DIR=""
|
||||
WORKSPACE_DIR=""
|
||||
APPLY_ENV=0
|
||||
STOP_FIRST=1
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--archive)
|
||||
ARCHIVE_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--repo-root)
|
||||
REPO_ROOT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--env-file)
|
||||
ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--config-dir)
|
||||
CONFIG_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--workspace-dir)
|
||||
WORKSPACE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--apply-env)
|
||||
APPLY_ENV=1
|
||||
shift
|
||||
;;
|
||||
--no-stop)
|
||||
STOP_FIRST=0
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail "Unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$ARCHIVE_PATH" ]] || fail "--archive is required"
|
||||
|
||||
require_cmd tar
|
||||
require_cmd rsync
|
||||
require_cmd shasum
|
||||
require_cmd python3
|
||||
require_cmd date
|
||||
|
||||
ARCHIVE_PATH="$(resolve_abs_path "$ARCHIVE_PATH")"
|
||||
REPO_ROOT="$(resolve_abs_path "$REPO_ROOT")"
|
||||
ENV_FILE="$(resolve_abs_path "$ENV_FILE")"
|
||||
|
||||
[[ -f "$ARCHIVE_PATH" ]] || fail "Archive not found: $ARCHIVE_PATH"
|
||||
[[ -d "$REPO_ROOT" ]] || fail "Repo root does not exist: $REPO_ROOT"
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
echo "==> Extracting archive"
|
||||
tar -xzf "$ARCHIVE_PATH" -C "$tmpdir"
|
||||
|
||||
[[ -f "$tmpdir/SHA256SUMS" ]] || fail "Archive missing SHA256SUMS"
|
||||
(
|
||||
cd "$tmpdir"
|
||||
shasum -a 256 -c SHA256SUMS
|
||||
)
|
||||
|
||||
if [[ -z "$CONFIG_DIR" ]]; then
|
||||
CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$(env_value_from_file "$ENV_FILE" OPENCLAW_CONFIG_DIR)}"
|
||||
fi
|
||||
if [[ -z "$WORKSPACE_DIR" ]]; then
|
||||
WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$(env_value_from_file "$ENV_FILE" OPENCLAW_WORKSPACE_DIR)}"
|
||||
fi
|
||||
|
||||
CONFIG_DIR="${CONFIG_DIR:-$HOME/.openclaw}"
|
||||
WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
|
||||
CONFIG_DIR="$(resolve_abs_path "$CONFIG_DIR")"
|
||||
WORKSPACE_DIR="$(resolve_abs_path "$WORKSPACE_DIR")"
|
||||
|
||||
if [[ $STOP_FIRST -eq 1 ]] && command -v docker >/dev/null 2>&1; then
|
||||
echo "==> Stopping gateway container"
|
||||
docker compose -f "$REPO_ROOT/docker-compose.yml" stop openclaw-gateway >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
mkdir -p "$(dirname "$CONFIG_DIR")" "$(dirname "$WORKSPACE_DIR")"
|
||||
|
||||
if [[ -d "$CONFIG_DIR" ]]; then
|
||||
mv "$CONFIG_DIR" "${CONFIG_DIR}.pre-restore-${timestamp}"
|
||||
fi
|
||||
if [[ -d "$WORKSPACE_DIR" ]]; then
|
||||
mv "$WORKSPACE_DIR" "${WORKSPACE_DIR}.pre-restore-${timestamp}"
|
||||
fi
|
||||
|
||||
mkdir -p "$CONFIG_DIR" "$WORKSPACE_DIR"
|
||||
|
||||
echo "==> Restoring config"
|
||||
rsync -a "$tmpdir/payload/config/" "$CONFIG_DIR/"
|
||||
|
||||
echo "==> Restoring workspace"
|
||||
rsync -a "$tmpdir/payload/workspace/" "$WORKSPACE_DIR/"
|
||||
|
||||
if [[ -f "$tmpdir/payload/repo/.env" ]]; then
|
||||
if [[ $APPLY_ENV -eq 1 ]]; then
|
||||
mkdir -p "$(dirname "$ENV_FILE")"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
cp "$ENV_FILE" "${ENV_FILE}.pre-restore-${timestamp}"
|
||||
fi
|
||||
cp "$tmpdir/payload/repo/.env" "$ENV_FILE"
|
||||
echo "==> Applied backed up env file to $ENV_FILE"
|
||||
else
|
||||
cp "$tmpdir/payload/repo/.env" "${ENV_FILE}.from-backup"
|
||||
echo "==> Wrote env candidate to ${ENV_FILE}.from-backup"
|
||||
fi
|
||||
fi
|
||||
|
||||
source_arch="$(grep -E '^source_arch=' "$tmpdir/meta/backup.env" | cut -d= -f2- || true)"
|
||||
target_arch="$(uname -m)"
|
||||
if [[ -n "$source_arch" && "$source_arch" != "$target_arch" ]]; then
|
||||
echo
|
||||
echo "NOTE: source arch (${source_arch}) differs from target arch (${target_arch})."
|
||||
echo "Rebuild the Docker image on this host; do not reuse old binary caches or volumes."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Restore completed."
|
||||
echo "Next steps:"
|
||||
echo " 1) docker compose -f \"$REPO_ROOT/docker-compose.yml\" up -d --build --force-recreate openclaw-gateway"
|
||||
echo " 2) docker compose -f \"$REPO_ROOT/docker-compose.yml\" run --rm openclaw-cli health"
|
||||
echo " 3) docker compose -f \"$REPO_ROOT/docker-compose.yml\" run --rm openclaw-cli channels status --probe"
|
||||
116
scripts/openclaw-keepawake.sh
Executable file
116
scripts/openclaw-keepawake.sh
Executable file
@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PID_FILE="${OPENCLAW_KEEPAWAKE_PID_FILE:-$HOME/.openclaw/run/keepawake.pid}"
|
||||
KEEPAWAKE_FLAGS="${OPENCLAW_KEEPAWAKE_FLAGS:--imsu}"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/openclaw-keepawake.sh <on|off|status|restart>
|
||||
|
||||
Keeps macOS awake using `caffeinate` for long-running OpenClaw Docker sessions.
|
||||
|
||||
Commands:
|
||||
on Start keep-awake background process
|
||||
off Stop keep-awake background process
|
||||
status Show current keep-awake status
|
||||
restart Restart keep-awake background process
|
||||
|
||||
Environment:
|
||||
OPENCLAW_KEEPAWAKE_FLAGS caffeinate flags (default: -imsu)
|
||||
Use -dimsu to keep displays awake too.
|
||||
USAGE
|
||||
}
|
||||
|
||||
ensure_caffeinate() {
|
||||
if ! command -v caffeinate >/dev/null 2>&1; then
|
||||
echo "caffeinate not found (this helper is for macOS)." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
read_pid() {
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
tr -d '[:space:]' <"$PID_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
is_caffeinate_pid() {
|
||||
local pid="$1"
|
||||
[[ -n "$pid" ]] || return 1
|
||||
kill -0 "$pid" >/dev/null 2>&1 || return 1
|
||||
local comm
|
||||
comm="$(ps -p "$pid" -o comm= 2>/dev/null | tr -d '[:space:]')"
|
||||
[[ "$comm" == "caffeinate" ]]
|
||||
}
|
||||
|
||||
start_awake() {
|
||||
ensure_caffeinate
|
||||
mkdir -p "$(dirname "$PID_FILE")"
|
||||
|
||||
local pid
|
||||
pid="$(read_pid || true)"
|
||||
if is_caffeinate_pid "$pid"; then
|
||||
echo "keep-awake already on (pid $pid)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a flags
|
||||
# Allow override for special cases (for example keeping displays on).
|
||||
read -r -a flags <<<"$KEEPAWAKE_FLAGS"
|
||||
caffeinate "${flags[@]}" >/dev/null 2>&1 &
|
||||
pid="$!"
|
||||
echo "$pid" >"$PID_FILE"
|
||||
echo "keep-awake on (pid $pid, flags: $KEEPAWAKE_FLAGS)"
|
||||
}
|
||||
|
||||
stop_awake() {
|
||||
local pid
|
||||
pid="$(read_pid || true)"
|
||||
if [[ -z "$pid" ]]; then
|
||||
echo "keep-awake already off"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if is_caffeinate_pid "$pid"; then
|
||||
kill "$pid" >/dev/null 2>&1 || true
|
||||
echo "keep-awake off (stopped pid $pid)"
|
||||
else
|
||||
echo "keep-awake off (stale pid file removed)"
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
}
|
||||
|
||||
status_awake() {
|
||||
local pid
|
||||
pid="$(read_pid || true)"
|
||||
if is_caffeinate_pid "$pid"; then
|
||||
echo "keep-awake is on (pid $pid)"
|
||||
else
|
||||
echo "keep-awake is off"
|
||||
if [[ -n "$pid" ]]; then
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cmd="${1:-}"
|
||||
case "$cmd" in
|
||||
on)
|
||||
start_awake
|
||||
;;
|
||||
off)
|
||||
stop_awake
|
||||
;;
|
||||
status)
|
||||
status_awake
|
||||
;;
|
||||
restart)
|
||||
stop_awake
|
||||
start_awake
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@ -7,12 +7,14 @@ metadata:
|
||||
"openclaw":
|
||||
{
|
||||
"emoji": "🧾",
|
||||
"os": ["darwin"],
|
||||
"requires": { "bins": ["summarize"] },
|
||||
"install":
|
||||
[
|
||||
{
|
||||
"id": "brew",
|
||||
"kind": "brew",
|
||||
"os": ["darwin"],
|
||||
"formula": "steipete/tap/summarize",
|
||||
"bins": ["summarize"],
|
||||
"label": "Install summarize (brew)",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import "./isolated-agent.mocks.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import * as modelSelection from "../agents/model-selection.js";
|
||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
@ -11,6 +11,7 @@ import {
|
||||
withTempCronHome,
|
||||
writeSessionStoreEntries,
|
||||
} from "./isolated-agent.test-harness.js";
|
||||
import { setupIsolatedAgentTurnMocks } from "./isolated-agent.test-setup.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
const withTempHome = withTempCronHome;
|
||||
@ -126,11 +127,16 @@ async function expectInvalidModel(home: string, model: string) {
|
||||
|
||||
describe("cron model formatting and precedence edge cases", () => {
|
||||
beforeEach(() => {
|
||||
setupIsolatedAgentTurnMocks({ fast: true });
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue("off");
|
||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
||||
vi.mocked(loadModelCatalog).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
// ------ provider/model string splitting ------
|
||||
|
||||
describe("parseModelRef formatting", () => {
|
||||
|
||||
@ -27,6 +27,7 @@ describe("Dockerfile", () => {
|
||||
const dockerfile = await readFile(dockerfilePath, "utf8");
|
||||
const installIndex = dockerfile.indexOf("pnpm install --frozen-lockfile");
|
||||
const browserArgIndex = dockerfile.indexOf("ARG OPENCLAW_INSTALL_BROWSER");
|
||||
const baseAptPackagesMatch = dockerfile.match(/BASE_APT_PACKAGES="([\s\S]*?)";/);
|
||||
|
||||
expect(installIndex).toBeGreaterThan(-1);
|
||||
expect(browserArgIndex).toBeGreaterThan(-1);
|
||||
@ -34,7 +35,9 @@ describe("Dockerfile", () => {
|
||||
expect(dockerfile).toContain(
|
||||
"node /app/node_modules/playwright-core/cli.js install --with-deps chromium",
|
||||
);
|
||||
expect(dockerfile).toContain("apt-get install -y --no-install-recommends xvfb");
|
||||
expect(baseAptPackagesMatch).not.toBeNull();
|
||||
expect(baseAptPackagesMatch?.[1]).toContain("xvfb");
|
||||
expect(dockerfile).toContain("apt-get install -y --no-install-recommends ${BASE_APT_PACKAGES}");
|
||||
});
|
||||
|
||||
it("prunes runtime dependencies after the build stage", async () => {
|
||||
|
||||
@ -1,22 +1,14 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPluginSdkEntrySources,
|
||||
buildPluginSdkPackageExports,
|
||||
buildPluginSdkSpecifiers,
|
||||
pluginSdkEntrypoints,
|
||||
} from "./entrypoints.js";
|
||||
import { buildPluginSdkPackageExports, buildPluginSdkSpecifiers } from "./entrypoints.js";
|
||||
|
||||
const pluginSdkSpecifiers = buildPluginSdkSpecifiers();
|
||||
const execFileAsync = promisify(execFile);
|
||||
const require = createRequire(import.meta.url);
|
||||
const tsdownModuleUrl = pathToFileURL(require.resolve("tsdown")).href;
|
||||
|
||||
describe("plugin-sdk bundled exports", () => {
|
||||
it("emits importable bundled subpath entries", { timeout: 240_000 }, async () => {
|
||||
@ -24,24 +16,9 @@ describe("plugin-sdk bundled exports", () => {
|
||||
const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-consumer-"));
|
||||
|
||||
try {
|
||||
const buildScriptPath = path.join(fixtureDir, "build-plugin-sdk.mjs");
|
||||
await fs.writeFile(
|
||||
buildScriptPath,
|
||||
`import { build } from ${JSON.stringify(tsdownModuleUrl)};
|
||||
await build(${JSON.stringify({
|
||||
clean: true,
|
||||
config: false,
|
||||
dts: false,
|
||||
entry: buildPluginSdkEntrySources(),
|
||||
env: { NODE_ENV: "production" },
|
||||
fixedExtension: false,
|
||||
logLevel: "error",
|
||||
outDir,
|
||||
platform: "node",
|
||||
})});
|
||||
`,
|
||||
);
|
||||
await execFileAsync(process.execPath, [buildScriptPath], {
|
||||
// Reuse the repo's tsdown config so plugin-sdk bundle checks stay aligned
|
||||
// with the production build graph and singleton boundaries.
|
||||
await execFileAsync(process.execPath, ["scripts/tsdown-build.mjs", "--outDir", outDir], {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
await fs.symlink(
|
||||
@ -50,17 +27,12 @@ await build(${JSON.stringify({
|
||||
"dir",
|
||||
);
|
||||
|
||||
for (const entry of pluginSdkEntrypoints) {
|
||||
const module = await import(pathToFileURL(path.join(outDir, `${entry}.js`)).href);
|
||||
expect(module).toBeTypeOf("object");
|
||||
}
|
||||
|
||||
const packageDir = path.join(fixtureDir, "openclaw");
|
||||
const consumerDir = path.join(fixtureDir, "consumer");
|
||||
const consumerEntry = path.join(consumerDir, "import-plugin-sdk.mjs");
|
||||
|
||||
await fs.mkdir(path.join(packageDir, "dist"), { recursive: true });
|
||||
await fs.symlink(outDir, path.join(packageDir, "dist", "plugin-sdk"), "dir");
|
||||
await fs.mkdir(packageDir, { recursive: true });
|
||||
await fs.symlink(outDir, path.join(packageDir, "dist"), "dir");
|
||||
// Mirror the installed package layout so subpaths can resolve root deps.
|
||||
await fs.symlink(
|
||||
path.join(process.cwd(), "node_modules"),
|
||||
|
||||
@ -289,6 +289,7 @@ describe("secrets runtime snapshot integration", () => {
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: {
|
||||
|
||||
@ -37,6 +37,11 @@ describe("git-hooks/pre-commit (integration)", () => {
|
||||
path.join(process.cwd(), "git-hooks", "pre-commit"),
|
||||
path.join(dir, "git-hooks", "pre-commit"),
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(dir, "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw-pre-commit-test", private: true }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(dir, "scripts", "pre-commit", "run-node-tool.sh"),
|
||||
"#!/usr/bin/env bash\nexit 0\n",
|
||||
@ -56,6 +61,9 @@ describe("git-hooks/pre-commit (integration)", () => {
|
||||
// The hook ends with `pnpm check`, but this fixture is only exercising staged-file handling.
|
||||
// Stub pnpm too so Windows CI does not invoke a real package-manager command in the temp repo.
|
||||
writeExecutable(fakeBinDir, "pnpm", "#!/usr/bin/env bash\nexit 0\n");
|
||||
writeFileSync(path.join(fakeBinDir, "pnpm.cmd"), "@echo off\r\nexit /b 0\r\n", {
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
// Create an untracked file that should NOT be staged by the hook.
|
||||
writeFileSync(path.join(dir, "secret.txt"), "do-not-stage\n", "utf8");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user