diff --git a/CHANGELOG.md b/CHANGELOG.md index 055775883c3..85916a30d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai - Browser: surface pending and recently handled modal dialogs in snapshots, return `blockedByDialog` when an action opens a modal, and allow `browser dialog --dialog-id` to answer pending dialogs. - Browser CLI: add `openclaw browser evaluate --timeout-ms` so long-running page functions can extend both the evaluate action and request timeout budgets. (#83447) Thanks @eefreenyc. - Codex app-server: scope OpenClaw prompt guidance by runtime surface so native Codex keeps Codex-owned base/personality instructions while OpenClaw contributes only runtime context, delivery guidance, and explicitly scoped command hints. (#83454) Thanks @100yenadmin. +- Docker/Podman: add `OPENCLAW_IMAGE_PIP_PACKAGES` for opt-in Python package installation in local image builds. (#83771) Thanks @stephenredmond-straiteis. - Agents/tools: shorten built-in tool descriptions and schema hints across media, messaging, sessions, cron, Gateway, web, image/PDF, TTS, nodes, and plan tools while preserving routing guardrails. - Skills: add node inspector debugging, fused diagram generation, and throwaway spike workflow skills. - CLI/plugins: add `defineToolPlugin` plus `openclaw plugins build`, `validate`, and `init` for typed simple tool plugins with generated manifest metadata, optional tool declarations, and context factories. diff --git a/Dockerfile b/Dockerfile index 8695ff68e06..e85a49c649f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -210,6 +210,19 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $packages; \ fi +# Install additional Python packages needed by your plugins or skills. +# Example: docker build --build-arg OPENCLAW_IMAGE_PIP_PACKAGES="requests humanize" . +ARG OPENCLAW_IMAGE_PIP_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_IMAGE_PIP_PACKAGES" ]; then \ + if ! python3 -m pip --version >/dev/null 2>&1; then \ + apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends python3-pip; \ + fi && \ + python3 -m pip install --no-cache-dir --break-system-packages $OPENCLAW_IMAGE_PIP_PACKAGES; \ + fi + # Optionally install Chromium and Xvfb for browser automation. # Build with: docker build --build-arg OPENCLAW_INSTALL_BROWSER=1 ... # Adds ~300MB but eliminates the 60-90s Playwright install on every container start. diff --git a/docs/install/docker.md b/docs/install/docker.md index 806f095105f..7fecbefe4e3 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -127,6 +127,7 @@ The setup script accepts these optional environment variables: | ------------------------------------------ | --------------------------------------------------------------------- | | `OPENCLAW_IMAGE` | Use a remote image instead of building locally | | `OPENCLAW_IMAGE_APT_PACKAGES` | Install extra apt packages during build (space-separated) | +| `OPENCLAW_IMAGE_PIP_PACKAGES` | Install extra Python packages during build (space-separated) | | `OPENCLAW_EXTENSIONS` | Pre-install plugin dependencies 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 | @@ -148,6 +149,9 @@ container without `brew`; those dependencies must be provided by a custom image or installed manually. For dependencies available from Debian packages, use `OPENCLAW_IMAGE_APT_PACKAGES` during image build. The legacy `OPENCLAW_DOCKER_APT_PACKAGES` name is still accepted. +For Python dependencies, use `OPENCLAW_IMAGE_PIP_PACKAGES`. This runs +`python3 -m pip install --break-system-packages` during the image build, so pin +package versions and use only package indexes you trust. Maintainers can test bundled plugin source against a packaged image by mounting one plugin source directory over its packaged source path, for example @@ -423,13 +427,14 @@ See [ClawDock](/install/clawdock) for the full helper guide. 1. **Persist `/home/node`**: `export OPENCLAW_HOME_VOLUME="openclaw_home"` 2. **Bake system deps**: `export OPENCLAW_IMAGE_APT_PACKAGES="git curl jq"` - 3. **Bake Playwright Chromium**: `export OPENCLAW_INSTALL_BROWSER=1` - 4. **Or install Playwright browsers into a persisted volume**: + 3. **Bake Python deps**: `export OPENCLAW_IMAGE_PIP_PACKAGES="requests==2.32.5 humanize==4.14.0"` + 4. **Bake Playwright Chromium**: `export OPENCLAW_INSTALL_BROWSER=1` + 5. **Or install Playwright browsers into a persisted volume**: ```bash docker compose run --rm openclaw-cli \ node /app/node_modules/playwright-core/cli.js install chromium ``` - 5. **Persist browser downloads**: use `OPENCLAW_HOME_VOLUME` or + 6. **Persist browser downloads**: use `OPENCLAW_HOME_VOLUME` or `OPENCLAW_EXTRA_MOUNTS`. OpenClaw auto-detects the Docker image's Playwright-managed Chromium on Linux. diff --git a/docs/install/podman.md b/docs/install/podman.md index 5c663a85f56..3bea007a12b 100644 --- a/docs/install/podman.md +++ b/docs/install/podman.md @@ -62,6 +62,7 @@ Optional build/setup env vars: - `OPENCLAW_IMAGE` or `OPENCLAW_PODMAN_IMAGE` -- use an existing/pulled image instead of building `openclaw:local` - `OPENCLAW_IMAGE_APT_PACKAGES` -- install extra apt packages during image build (also accepts legacy `OPENCLAW_DOCKER_APT_PACKAGES`) +- `OPENCLAW_IMAGE_PIP_PACKAGES` -- install extra Python packages during image build; pin versions and use only package indexes you trust - `OPENCLAW_EXTENSIONS` -- pre-install plugin dependencies at build time - `OPENCLAW_INSTALL_BROWSER` -- pre-install Chromium and Xvfb for browser automation (set to `1` to enable) diff --git a/scripts/clawdock/README.md b/scripts/clawdock/README.md index d27d05ddcec..90724ec0aa2 100644 --- a/scripts/clawdock/README.md +++ b/scripts/clawdock/README.md @@ -180,9 +180,10 @@ vim ~/.openclaw/.env See `.env.example` for all supported keys. -The `Dockerfile` supports two optional build args: +The `Dockerfile` supports optional build args: - `OPENCLAW_IMAGE_APT_PACKAGES` — extra apt packages to install (e.g. `ffmpeg`); also accepts legacy `OPENCLAW_DOCKER_APT_PACKAGES` +- `OPENCLAW_IMAGE_PIP_PACKAGES` — extra Python packages to install (e.g. `requests==2.32.5`); pin versions and use only package indexes you trust - `OPENCLAW_INSTALL_BROWSER=1` — pre-install Chromium for browser automation (adds ~300MB, but skips the 60-90s Playwright install on each container start) ### How It Works in Docker diff --git a/scripts/docker/setup.sh b/scripts/docker/setup.sh index 0318acf6744..af454e7a1bd 100755 --- a/scripts/docker/setup.sh +++ b/scripts/docker/setup.sh @@ -297,6 +297,7 @@ export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}" export OPENCLAW_DISABLE_BONJOUR="${OPENCLAW_DISABLE_BONJOUR:-}" export OPENCLAW_IMAGE="$IMAGE_NAME" export OPENCLAW_IMAGE_APT_PACKAGES="${OPENCLAW_IMAGE_APT_PACKAGES-${OPENCLAW_DOCKER_APT_PACKAGES:-}}" +export OPENCLAW_IMAGE_PIP_PACKAGES="${OPENCLAW_IMAGE_PIP_PACKAGES:-}" export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}" export OPENCLAW_INSTALL_BROWSER="${OPENCLAW_INSTALL_BROWSER:-}" export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS" @@ -501,6 +502,7 @@ upsert_env "$ENV_FILE" \ OPENCLAW_EXTRA_MOUNTS \ OPENCLAW_HOME_VOLUME \ OPENCLAW_IMAGE_APT_PACKAGES \ + OPENCLAW_IMAGE_PIP_PACKAGES \ OPENCLAW_EXTENSIONS \ OPENCLAW_INSTALL_BROWSER \ OPENCLAW_SANDBOX \ @@ -523,6 +525,7 @@ if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then echo "==> Building Docker image: $IMAGE_NAME" run_docker_build \ --build-arg "OPENCLAW_IMAGE_APT_PACKAGES=${OPENCLAW_IMAGE_APT_PACKAGES}" \ + --build-arg "OPENCLAW_IMAGE_PIP_PACKAGES=${OPENCLAW_IMAGE_PIP_PACKAGES}" \ --build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \ --build-arg "OPENCLAW_INSTALL_BROWSER=${OPENCLAW_INSTALL_BROWSER}" \ --build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \ diff --git a/scripts/podman/setup.sh b/scripts/podman/setup.sh index 8cc3d5f0039..ac8c8e8fb57 100755 --- a/scripts/podman/setup.sh +++ b/scripts/podman/setup.sh @@ -359,10 +359,14 @@ ensure_private_existing_dir_owned_by_user "config directory" "$OPENCLAW_CONFIG_D ensure_private_existing_dir_owned_by_user "workspace directory" "$OPENCLAW_WORKSPACE_DIR" OPENCLAW_IMAGE_APT_PACKAGES="${OPENCLAW_IMAGE_APT_PACKAGES-${OPENCLAW_DOCKER_APT_PACKAGES:-}}" +OPENCLAW_IMAGE_PIP_PACKAGES="${OPENCLAW_IMAGE_PIP_PACKAGES:-}" BUILD_ARGS=() if [[ -n "$OPENCLAW_IMAGE_APT_PACKAGES" ]]; then BUILD_ARGS+=(--build-arg "OPENCLAW_IMAGE_APT_PACKAGES=${OPENCLAW_IMAGE_APT_PACKAGES}") fi +if [[ -n "$OPENCLAW_IMAGE_PIP_PACKAGES" ]]; then + BUILD_ARGS+=(--build-arg "OPENCLAW_IMAGE_PIP_PACKAGES=${OPENCLAW_IMAGE_PIP_PACKAGES}") +fi if [[ -n "${OPENCLAW_EXTENSIONS:-}" ]]; then BUILD_ARGS+=(--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}") fi diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index cbf8d000b1d..7a614d81c62 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; const SCRIPT_PATH = "scripts/test-install-sh-docker.sh"; const DOCKER_SETUP_PATH = "scripts/docker/setup.sh"; +const PODMAN_SETUP_PATH = "scripts/podman/setup.sh"; const SMOKE_RUNNER_PATH = "scripts/docker/install-sh-smoke/run.sh"; const BUN_GLOBAL_SMOKE_PATH = "scripts/e2e/bun-global-install-smoke.sh"; const BUN_GLOBAL_ASSERTIONS_PATH = "scripts/e2e/lib/bun-global-install/assertions.mjs"; @@ -95,6 +96,30 @@ describe("test-install-sh-docker", () => { expect(script).toContain('--build-arg "OPENCLAW_INSTALL_BROWSER=${OPENCLAW_INSTALL_BROWSER}"'); }); + it("passes image-scoped pip packages through Docker and Podman setup", () => { + const dockerSetup = readFileSync(DOCKER_SETUP_PATH, "utf8"); + const podmanSetup = readFileSync(PODMAN_SETUP_PATH, "utf8"); + const dockerfile = readFileSync("Dockerfile", "utf8"); + + expect(dockerfile).toContain("ARG OPENCLAW_IMAGE_PIP_PACKAGES"); + expect(dockerfile).toContain( + "python3 -m pip install --no-cache-dir --break-system-packages $OPENCLAW_IMAGE_PIP_PACKAGES", + ); + expect(dockerSetup).toContain( + 'export OPENCLAW_IMAGE_PIP_PACKAGES="${OPENCLAW_IMAGE_PIP_PACKAGES:-}"', + ); + expect(dockerSetup).toContain("OPENCLAW_IMAGE_PIP_PACKAGES \\"); + expect(dockerSetup).toContain( + '--build-arg "OPENCLAW_IMAGE_PIP_PACKAGES=${OPENCLAW_IMAGE_PIP_PACKAGES}"', + ); + expect(dockerSetup).not.toContain("OPENCLAW_DOCKER_PIP_PACKAGES"); + expect(podmanSetup).toContain('OPENCLAW_IMAGE_PIP_PACKAGES="${OPENCLAW_IMAGE_PIP_PACKAGES:-}"'); + expect(podmanSetup).toContain( + 'BUILD_ARGS+=(--build-arg "OPENCLAW_IMAGE_PIP_PACKAGES=${OPENCLAW_IMAGE_PIP_PACKAGES}")', + ); + expect(podmanSetup).not.toContain("OPENCLAW_DOCKER_PIP_PACKAGES"); + }); + it("allows repository branch history and release tags for secret-backed Docker release checks", () => { const workflow = readFileSync(LIVE_E2E_WORKFLOW_PATH, "utf8");