fix(docker): allow spaces in setup mount paths

Allow ordinary spaces in Docker setup host persistence paths while preserving control-character and comma mount guards. Quote generated and base Compose volume scalars so OPENCLAW_CONFIG_DIR, OPENCLAW_WORKSPACE_DIR, and auth-profile secret paths can be parsed when host directories contain spaces.
This commit is contained in:
Vincent Koc 2026-06-06 07:31:53 -07:00
parent 2a48a2655b
commit f02750d162
No known key found for this signature in database
3 changed files with 64 additions and 23 deletions

View file

@ -39,9 +39,9 @@ services:
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
TZ: ${OPENCLAW_TZ:-UTC}
volumes:
- ${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace
- ${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw
- "${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw"
- "${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace"
- "${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw"
## 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
@ -118,9 +118,9 @@ services:
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
TZ: ${OPENCLAW_TZ:-UTC}
volumes:
- ${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace
- ${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw
- "${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw"
- "${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace"
- "${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw"
stdin_open: true
tty: true
init: true

View file

@ -218,9 +218,6 @@ validate_mount_path_value() {
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() {
@ -237,11 +234,18 @@ validate_mount_spec() {
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."
if [[ ! "$mount" =~ ^[^,:]+:[^,:]+(:[^,:]+)?$ ]]; then
fail "Invalid mount format '$mount'. Expected source:target[:options] without commas or control characters."
fi
}
quote_yaml_string() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
printf '"%s"' "$value"
}
require_cmd docker
if ! docker compose version >/dev/null 2>&1; then
echo "Docker Compose not available (try: docker compose version)" >&2
@ -388,15 +392,15 @@ YAML
validate_mount_spec "$gateway_config_mount"
validate_mount_spec "$gateway_workspace_mount"
validate_mount_spec "$gateway_auth_profile_secret_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"
printf ' - %s\n' "$gateway_auth_profile_secret_mount" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$(quote_yaml_string "$gateway_home_mount")" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$(quote_yaml_string "$gateway_config_mount")" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$(quote_yaml_string "$gateway_workspace_mount")" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$(quote_yaml_string "$gateway_auth_profile_secret_mount")" >>"$EXTRA_COMPOSE_FILE"
fi
for mount in "$@"; do
validate_mount_spec "$mount"
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$(quote_yaml_string "$mount")" >>"$EXTRA_COMPOSE_FILE"
done
cat >>"$EXTRA_COMPOSE_FILE" <<'YAML'
@ -405,15 +409,15 @@ YAML
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"
printf ' - %s\n' "$gateway_auth_profile_secret_mount" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$(quote_yaml_string "$gateway_home_mount")" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$(quote_yaml_string "$gateway_config_mount")" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$(quote_yaml_string "$gateway_workspace_mount")" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$(quote_yaml_string "$gateway_auth_profile_secret_mount")" >>"$EXTRA_COMPOSE_FILE"
fi
for mount in "$@"; do
validate_mount_spec "$mount"
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s\n' "$(quote_yaml_string "$mount")" >>"$EXTRA_COMPOSE_FILE"
done
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
@ -658,7 +662,7 @@ if [[ -n "$SANDBOX_ENABLED" ]]; then
services:
openclaw-gateway:
volumes:
- ${DOCKER_SOCKET_PATH}:/var/run/docker.sock
- $(quote_yaml_string "${DOCKER_SOCKET_PATH}:/var/run/docker.sock")
YAML
if [[ -n "${DOCKER_GID:-}" ]]; then
cat >>"$SANDBOX_COMPOSE_FILE" <<YAML

View file

@ -303,6 +303,43 @@ describe("scripts/docker/setup.sh", () => {
expect(log).not.toContain("run --rm openclaw-cli onboard --mode local --no-install-daemon");
});
it("allows ordinary spaces in host persistence paths and quotes generated mounts", async () => {
const activeSandbox = requireSandbox(sandbox);
await resetDockerLog(activeSandbox);
const configDir = join(activeSandbox.rootDir, "config with spaces");
const workspaceDir = join(activeSandbox.rootDir, "workspace with spaces");
const authProfileSecretDir = join(activeSandbox.rootDir, "auth secrets with spaces");
const homeVolumeDir = join(activeSandbox.rootDir, "home volume with spaces");
const extraMountSource = join(activeSandbox.rootDir, "extra data");
const result = runDockerSetup(activeSandbox, {
OPENCLAW_CONFIG_DIR: configDir,
OPENCLAW_WORKSPACE_DIR: workspaceDir,
OPENCLAW_AUTH_PROFILE_SECRET_DIR: authProfileSecretDir,
OPENCLAW_HOME_VOLUME: homeVolumeDir,
OPENCLAW_EXTRA_MOUNTS: `${extraMountSource}:/mnt/extra data:ro`,
});
expect(result.status).toBe(0);
expect(result.stderr).not.toContain("cannot contain whitespace");
const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8");
expect(envFile).toContain(`OPENCLAW_CONFIG_DIR=${configDir}`);
expect(envFile).toContain(`OPENCLAW_WORKSPACE_DIR=${workspaceDir}`);
expect(envFile).toContain(`OPENCLAW_AUTH_PROFILE_SECRET_DIR=${authProfileSecretDir}`);
const extraCompose = await readFile(
join(activeSandbox.rootDir, "docker-compose.extra.yml"),
"utf8",
);
expect(extraCompose).toContain(`"${homeVolumeDir}:/home/node"`);
expect(extraCompose).toContain(`"${configDir}:/home/node/.openclaw"`);
expect(extraCompose).toContain(`"${workspaceDir}:/home/node/.openclaw/workspace"`);
expect(extraCompose).toContain(
`"${authProfileSecretDir}:/home/node/.config/openclaw"`,
);
expect(extraCompose).toContain(`"${extraMountSource}:/mnt/extra data:ro"`);
});
it("persists explicit Docker Bonjour opt-in overrides", async () => {
const activeSandbox = requireSandbox(sandbox);
@ -781,7 +818,7 @@ describe("scripts/docker/setup.sh", () => {
const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8");
expect(
compose.split(
"${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw",
'"${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw"',
),
).toHaveLength(3);
});