mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
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:
+6
-6
@@ -39,9 +39,9 @@ services:
|
|||||||
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
|
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
|
||||||
TZ: ${OPENCLAW_TZ:-UTC}
|
TZ: ${OPENCLAW_TZ:-UTC}
|
||||||
volumes:
|
volumes:
|
||||||
- ${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw
|
- "${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw"
|
||||||
- ${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace
|
- "${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_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw"
|
||||||
## Uncomment the lines below to enable sandbox isolation
|
## Uncomment the lines below to enable sandbox isolation
|
||||||
## (agents.defaults.sandbox). Requires Docker CLI in the image
|
## (agents.defaults.sandbox). Requires Docker CLI in the image
|
||||||
## (build with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1) or use
|
## (build with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1) or use
|
||||||
@@ -118,9 +118,9 @@ services:
|
|||||||
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
|
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
|
||||||
TZ: ${OPENCLAW_TZ:-UTC}
|
TZ: ${OPENCLAW_TZ:-UTC}
|
||||||
volumes:
|
volumes:
|
||||||
- ${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw
|
- "${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw"
|
||||||
- ${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace
|
- "${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_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw"
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
init: true
|
init: true
|
||||||
|
|||||||
+20
-16
@@ -218,9 +218,6 @@ validate_mount_path_value() {
|
|||||||
if contains_disallowed_chars "$value"; then
|
if contains_disallowed_chars "$value"; then
|
||||||
fail "$label contains unsupported control characters."
|
fail "$label contains unsupported control characters."
|
||||||
fi
|
fi
|
||||||
if [[ "$value" =~ [[:space:]] ]]; then
|
|
||||||
fail "$label cannot contain whitespace."
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_named_volume() {
|
validate_named_volume() {
|
||||||
@@ -237,11 +234,18 @@ validate_mount_spec() {
|
|||||||
fi
|
fi
|
||||||
# Keep mount specs strict to avoid YAML structure injection.
|
# Keep mount specs strict to avoid YAML structure injection.
|
||||||
# Expected format: source:target[:options]
|
# Expected format: source:target[:options]
|
||||||
if [[ ! "$mount" =~ ^[^[:space:],:]+:[^[:space:],:]+(:[^[:space:],:]+)?$ ]]; then
|
if [[ ! "$mount" =~ ^[^,:]+:[^,:]+(:[^,:]+)?$ ]]; then
|
||||||
fail "Invalid mount format '$mount'. Expected source:target[:options] without spaces."
|
fail "Invalid mount format '$mount'. Expected source:target[:options] without commas or control characters."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quote_yaml_string() {
|
||||||
|
local value="$1"
|
||||||
|
value="${value//\\/\\\\}"
|
||||||
|
value="${value//\"/\\\"}"
|
||||||
|
printf '"%s"' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
require_cmd docker
|
require_cmd docker
|
||||||
if ! docker compose version >/dev/null 2>&1; then
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
echo "Docker Compose not available (try: docker compose version)" >&2
|
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_config_mount"
|
||||||
validate_mount_spec "$gateway_workspace_mount"
|
validate_mount_spec "$gateway_workspace_mount"
|
||||||
validate_mount_spec "$gateway_auth_profile_secret_mount"
|
validate_mount_spec "$gateway_auth_profile_secret_mount"
|
||||||
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$(quote_yaml_string "$gateway_home_mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||||
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$(quote_yaml_string "$gateway_config_mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||||
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$(quote_yaml_string "$gateway_workspace_mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||||
printf ' - %s\n' "$gateway_auth_profile_secret_mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$(quote_yaml_string "$gateway_auth_profile_secret_mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for mount in "$@"; do
|
for mount in "$@"; do
|
||||||
validate_mount_spec "$mount"
|
validate_mount_spec "$mount"
|
||||||
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$(quote_yaml_string "$mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||||
done
|
done
|
||||||
|
|
||||||
cat >>"$EXTRA_COMPOSE_FILE" <<'YAML'
|
cat >>"$EXTRA_COMPOSE_FILE" <<'YAML'
|
||||||
@@ -405,15 +409,15 @@ YAML
|
|||||||
YAML
|
YAML
|
||||||
|
|
||||||
if [[ -n "$home_volume" ]]; then
|
if [[ -n "$home_volume" ]]; then
|
||||||
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$(quote_yaml_string "$gateway_home_mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||||
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$(quote_yaml_string "$gateway_config_mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||||
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$(quote_yaml_string "$gateway_workspace_mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||||
printf ' - %s\n' "$gateway_auth_profile_secret_mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$(quote_yaml_string "$gateway_auth_profile_secret_mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for mount in "$@"; do
|
for mount in "$@"; do
|
||||||
validate_mount_spec "$mount"
|
validate_mount_spec "$mount"
|
||||||
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$(quote_yaml_string "$mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
|
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
|
||||||
@@ -658,7 +662,7 @@ if [[ -n "$SANDBOX_ENABLED" ]]; then
|
|||||||
services:
|
services:
|
||||||
openclaw-gateway:
|
openclaw-gateway:
|
||||||
volumes:
|
volumes:
|
||||||
- ${DOCKER_SOCKET_PATH}:/var/run/docker.sock
|
- $(quote_yaml_string "${DOCKER_SOCKET_PATH}:/var/run/docker.sock")
|
||||||
YAML
|
YAML
|
||||||
if [[ -n "${DOCKER_GID:-}" ]]; then
|
if [[ -n "${DOCKER_GID:-}" ]]; then
|
||||||
cat >>"$SANDBOX_COMPOSE_FILE" <<YAML
|
cat >>"$SANDBOX_COMPOSE_FILE" <<YAML
|
||||||
|
|||||||
@@ -303,6 +303,43 @@ describe("scripts/docker/setup.sh", () => {
|
|||||||
expect(log).not.toContain("run --rm openclaw-cli onboard --mode local --no-install-daemon");
|
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 () => {
|
it("persists explicit Docker Bonjour opt-in overrides", async () => {
|
||||||
const activeSandbox = requireSandbox(sandbox);
|
const activeSandbox = requireSandbox(sandbox);
|
||||||
|
|
||||||
@@ -781,7 +818,7 @@ describe("scripts/docker/setup.sh", () => {
|
|||||||
const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8");
|
const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8");
|
||||||
expect(
|
expect(
|
||||||
compose.split(
|
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);
|
).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user