name: openclaw filesystem write gate # Detect when the upstream openclaw image starts writing to NEW paths under # /home/node/.openclaw/ that the compose volume layout does not account for. # # Background: issue #498 originally proposed using `docker diff` to detect # openclaw writes outside known paths. `docker diff` is structurally unable # to see writes into volumes (named or bind), so it cannot observe writes to # /home/node/.openclaw/ regardless of volume type. # # Mechanism instead: bind-mount a host directory over /home/node/.openclaw/ # inside the container, run openclaw briefly so it writes its initial files, # then walk the host directory and fail if any file other than expected # ephemeral startup files shows up. State directories such as agents/, # canvas/, cron/, and workspace/ are mounted separately to mirror the # production layout, so they do not pollute the inspection. # # Companion to the OpenClaw home-volume simplification: keep generated config # and caches ephemeral, but fail when the image writes a state path that is not # explicitly backed by the compose layout. on: pull_request: paths: - 'ods/config/openclaw/**' - 'ods/extensions/services/openclaw/compose.yaml' - 'ods/extensions/services/openclaw/manifest.yaml' - '.github/workflows/openclaw-image-diff.yml' permissions: contents: read jobs: filesystem-write-gate: name: Detect openclaw writes outside known paths runs-on: ubuntu-latest timeout-minutes: 10 env: OPENCLAW_IMAGE: ghcr.io/openclaw/openclaw:2026.3.8 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Pull openclaw image run: docker pull "$OPENCLAW_IMAGE" - name: Prepare bind-mount layout run: | set -euo pipefail mkdir -p ./openclaw-test-home mkdir -p ./openclaw-test-agents mkdir -p ./openclaw-test-canvas mkdir -p ./openclaw-test-cron mkdir -p ./openclaw-test-data mkdir -p ./openclaw-test-workspace - name: Start openclaw with bind-mounted home run: | set -euo pipefail # Mirror the production compose user + entrypoint (root chown, then # drop to uid 1000 + docker-entrypoint.sh) so the same startup code # path runs. We do NOT use --rm: we want to capture logs after the # container exits if it crashed. docker run -d \ --name openclaw-test \ --user 0:0 \ -e OPENCLAW_CONFIG=/config/openclaw.json \ -e OPENCLAW_DATA=/data \ -e OPENCLAW_GATEWAY_TOKEN=ci-test-token \ -e OPENCLAW_TOKEN=ci-test-token \ -e LLM_MODEL=qwen3:30b-a3b \ -e OLLAMA_URL=http://llama-server:8080 \ -e HOME=/home/node \ -e OPENCLAW_EXTERNAL_PORT=7860 \ -e BIND_ADDRESS=127.0.0.1 \ -v "$(pwd)/ods/config/openclaw:/config:ro" \ -v "$(pwd)/openclaw-test-data:/data" \ -v "$(pwd)/openclaw-test-home:/home/node/.openclaw" \ -v "$(pwd)/openclaw-test-agents:/home/node/.openclaw/agents" \ -v "$(pwd)/openclaw-test-canvas:/home/node/.openclaw/canvas" \ -v "$(pwd)/openclaw-test-cron:/home/node/.openclaw/cron" \ -v "$(pwd)/openclaw-test-workspace:/home/node/.openclaw/workspace" \ --entrypoint /bin/sh \ "$OPENCLAW_IMAGE" \ -c "chown -R 1000:1000 /home/node/.openclaw; exec setpriv --reuid=1000 --regid=1000 --clear-groups /bin/sh -c 'node /config/inject-token.js; export OPENCLAW_CONFIG_PATH=/tmp/openclaw-config.json; exec docker-entrypoint.sh node openclaw.mjs gateway --allow-unconfigured --bind lan'" # Give the container time to write its initial files. The writes we # care about happen at startup, well within 30s. sleep 30 # Sanity-check that the container is still running. If it crashed, # the bind-mount may only contain writes from the wrapper script # and the audit can false-green without observing OpenClaw itself. if ! docker ps --filter name=openclaw-test --filter status=running --format '{{.Names}}' | grep -q '^openclaw-test$'; then echo "::error::openclaw-test is not running after 30s; refusing to audit an incomplete startup." echo "--- container logs ---" docker logs openclaw-test 2>&1 | tail -100 || true echo "--- end logs ---" exit 1 fi - name: Inspect bind-mount for unexpected writes if: always() run: | set -euo pipefail # Stop and remove the container so the bind-mount is quiescent. docker rm -f openclaw-test >/dev/null 2>&1 || true # OpenClaw creates some directories with the container user's # permissions. Normalize ownership so the host-side find can inspect # the bind mount without failing before the allowlist check runs. sudo chown -R "$(id -u):$(id -g)" \ ./openclaw-test-home \ ./openclaw-test-agents \ ./openclaw-test-canvas \ ./openclaw-test-cron \ ./openclaw-test-data \ ./openclaw-test-workspace # Allowlist: # - openclaw.json regenerated by openclaw on each container start # - update-check.json disposable upstream update-check cache # - agents/ bind-mounted separately to data/openclaw/home # - canvas/ bind-mounted separately to data/openclaw/home # - cron/ bind-mounted separately to data/openclaw/home # - workspace/ bind-mounted separately to config/openclaw # Anything else is a new write path that the compose volume layout # does not account for. UNEXPECTED=$(find ./openclaw-test-home -mindepth 1 -maxdepth 5 \ \( -path './openclaw-test-home/agents' \ -o -path './openclaw-test-home/canvas' \ -o -path './openclaw-test-home/cron' \ -o -path './openclaw-test-home/workspace' \) -prune -o \ -type f ! -name 'openclaw.json' ! -name 'update-check.json' -print) if [ -n "$UNEXPECTED" ]; then echo "::error::openclaw is writing to new paths under /home/node/.openclaw/. Update compose.yaml volume layout to include these paths or update this test's allowlist." echo "Unexpected files:" echo "$UNEXPECTED" exit 1 fi # Positive assertion: a successful audit must have observed openclaw.json being # written. If it's missing, the container exited before the entrypoint got # far enough to regenerate it, and the empty bind-mount tells us nothing # about openclaw's actual write paths. This is a write-observation gate, so # a container that produces no observable writes is a hard failure, not a # silent pass. Treating this as warning-only would let a future image bump # that crashes on startup ship undetected. if [ ! -f ./openclaw-test-home/openclaw.json ]; then echo "::error::openclaw.json was never written; container exited before producing the expected baseline write. The empty bind-mount makes this audit meaningless (false-green risk). Check the container logs above for the startup failure." exit 1 fi echo "openclaw filesystem writes match expected pattern (openclaw.json only)."