Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
966
scripts/docker/setup.sh
Executable file
966
scripts/docker/setup.sh
Executable file
@@ -0,0 +1,966 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/docker-build.sh"
|
||||
source "$ROOT_DIR/scripts/lib/host-timeout.sh"
|
||||
COMPOSE_FILE="$ROOT_DIR/docker-compose.yml"
|
||||
EXTRA_COMPOSE_FILE="$ROOT_DIR/docker-compose.extra.yml"
|
||||
IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}"
|
||||
EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}"
|
||||
HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}"
|
||||
RAW_SANDBOX_SETTING="${OPENCLAW_SANDBOX:-}"
|
||||
SANDBOX_ENABLED=""
|
||||
DOCKER_SOCKET_PATH="${OPENCLAW_DOCKER_SOCKET:-}"
|
||||
TIMEZONE="${OPENCLAW_TZ:-}"
|
||||
RAW_SKIP_ONBOARDING="${OPENCLAW_SKIP_ONBOARDING:-}"
|
||||
SKIP_ONBOARDING=""
|
||||
DOCKER_PULL_TIMEOUT="${OPENCLAW_DOCKER_SETUP_PULL_TIMEOUT:-600s}"
|
||||
OFFLINE_MODE=""
|
||||
DEFAULT_SANDBOX_IMAGE="openclaw-sandbox:bookworm-slim"
|
||||
DEFAULT_SANDBOX_BROWSER_IMAGE="openclaw-sandbox-browser:bookworm-slim"
|
||||
SANDBOX_BROWSER_IMAGE_CONTRACT_EPOCH="2026-05-12-cdp-relay-auth"
|
||||
|
||||
fail() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--offline)
|
||||
OFFLINE_MODE="1"
|
||||
;;
|
||||
*)
|
||||
fail "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Missing dependency: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_docker_build() {
|
||||
# Dockerfile uses BuildKit-only syntax (RUN --mount=type=cache). Force
|
||||
# BuildKit so hosts defaulting to the legacy builder do not fail.
|
||||
docker_build_exec "$@"
|
||||
}
|
||||
|
||||
run_docker_pull() {
|
||||
local image="$1"
|
||||
openclaw_host_timeout_cmd "$DOCKER_PULL_TIMEOUT" docker pull "$image"
|
||||
}
|
||||
|
||||
require_local_docker_image() {
|
||||
local image="$1"
|
||||
if docker image inspect "$image" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
fail "Offline Docker setup requires preloaded image $image. Load it with 'docker load -i <image.tar>' before running scripts/docker/setup.sh --offline."
|
||||
}
|
||||
|
||||
is_truthy_value() {
|
||||
local raw="${1:-}"
|
||||
raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
||||
case "$raw" in
|
||||
1 | true | yes | on) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
read_config_gateway_token() {
|
||||
local config_path="$OPENCLAW_CONFIG_DIR/openclaw.json"
|
||||
if [[ ! -f "$config_path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 - "$config_path" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
except Exception:
|
||||
raise SystemExit(0)
|
||||
|
||||
gateway = cfg.get("gateway")
|
||||
if not isinstance(gateway, dict):
|
||||
raise SystemExit(0)
|
||||
auth = gateway.get("auth")
|
||||
if not isinstance(auth, dict):
|
||||
raise SystemExit(0)
|
||||
token = auth.get("token")
|
||||
if isinstance(token, str):
|
||||
token = token.strip()
|
||||
if token:
|
||||
print(token)
|
||||
PY
|
||||
return 0
|
||||
fi
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
node - "$config_path" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const configPath = process.argv[2];
|
||||
try {
|
||||
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
const token = cfg?.gateway?.auth?.token;
|
||||
if (typeof token === "string" && token.trim().length > 0) {
|
||||
process.stdout.write(token.trim());
|
||||
}
|
||||
} catch {
|
||||
// Keep docker-setup resilient when config parsing fails.
|
||||
}
|
||||
NODE
|
||||
fi
|
||||
}
|
||||
|
||||
read_env_gateway_token() {
|
||||
local env_path="$1"
|
||||
local line=""
|
||||
local token=""
|
||||
if [[ ! -f "$env_path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%$'\r'}"
|
||||
if [[ "$line" == OPENCLAW_GATEWAY_TOKEN=* ]]; then
|
||||
token="${line#OPENCLAW_GATEWAY_TOKEN=}"
|
||||
fi
|
||||
done <"$env_path"
|
||||
if [[ -n "$token" ]]; then
|
||||
printf '%s' "$token"
|
||||
fi
|
||||
}
|
||||
|
||||
sync_gateway_config() {
|
||||
local allowed_origin_json=""
|
||||
local current_allowed_origins=""
|
||||
local batch_json=""
|
||||
|
||||
if [[ "${OPENCLAW_GATEWAY_BIND}" != "loopback" ]]; then
|
||||
allowed_origin_json="$(printf '["http://localhost:%s","http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT" "$OPENCLAW_GATEWAY_PORT")"
|
||||
current_allowed_origins="$(
|
||||
run_prestart_cli config get gateway.controlUi.allowedOrigins 2>/dev/null || true
|
||||
)"
|
||||
current_allowed_origins="${current_allowed_origins//$'\r'/}"
|
||||
fi
|
||||
|
||||
batch_json="$(printf '[{"path":"gateway.mode","value":"local"},{"path":"gateway.bind","value":"%s"}' "$OPENCLAW_GATEWAY_BIND")"
|
||||
if [[ -n "$allowed_origin_json" ]]; then
|
||||
if [[ -n "$current_allowed_origins" && "$current_allowed_origins" != "null" && "$current_allowed_origins" != "[]" ]]; then
|
||||
echo "Control UI allowlist already configured; leaving gateway.controlUi.allowedOrigins unchanged."
|
||||
else
|
||||
batch_json+=",{\"path\":\"gateway.controlUi.allowedOrigins\",\"value\":$allowed_origin_json}"
|
||||
fi
|
||||
fi
|
||||
batch_json+="]"
|
||||
|
||||
run_prestart_cli config set --batch-json "$batch_json" >/dev/null
|
||||
echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup."
|
||||
if [[ -n "$allowed_origin_json" ]]; then
|
||||
if [[ -z "$current_allowed_origins" || "$current_allowed_origins" == "null" || "$current_allowed_origins" == "[]" ]]; then
|
||||
echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
run_compose_one_off() {
|
||||
local -a run_args=(run)
|
||||
if [[ -n "$OFFLINE_MODE" ]]; then
|
||||
run_args+=(--pull never)
|
||||
fi
|
||||
docker compose "${COMPOSE_ARGS[@]}" "${run_args[@]}" "$@"
|
||||
}
|
||||
|
||||
run_prestart_gateway() {
|
||||
run_compose_one_off --rm --no-deps "$@"
|
||||
}
|
||||
|
||||
run_prestart_cli() {
|
||||
# During setup, avoid the shared-network openclaw-cli service because it
|
||||
# requires the gateway container's network namespace to already exist. That
|
||||
# creates a circular dependency for config writes that are needed before the
|
||||
# gateway can start cleanly.
|
||||
# Host OPENCLAW_* paths are Compose bind-mount sources. Setup-time CLI writes
|
||||
# must still resolve state/config paths inside the container.
|
||||
run_prestart_gateway \
|
||||
-e HOME=/home/node \
|
||||
-e OPENCLAW_HOME=/home/node \
|
||||
-e OPENCLAW_STATE_DIR=/home/node/.openclaw \
|
||||
-e OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json \
|
||||
-e OPENCLAW_CONFIG_DIR=/home/node/.openclaw \
|
||||
-e OPENCLAW_WORKSPACE_DIR=/home/node/.openclaw/workspace \
|
||||
--entrypoint node openclaw-gateway \
|
||||
dist/index.js "$@"
|
||||
}
|
||||
|
||||
run_runtime_cli() {
|
||||
local compose_scope="${1:-current}"
|
||||
local deps_mode="${2:-with-deps}"
|
||||
shift 2
|
||||
|
||||
local -a compose_args
|
||||
local -a run_args=(run)
|
||||
if [[ -n "$OFFLINE_MODE" ]]; then
|
||||
run_args+=(--pull never)
|
||||
fi
|
||||
run_args+=(--rm)
|
||||
|
||||
case "$compose_scope" in
|
||||
current) compose_args=("${COMPOSE_ARGS[@]}") ;;
|
||||
base) compose_args=("${BASE_COMPOSE_ARGS[@]}") ;;
|
||||
*) fail "Unknown runtime CLI compose scope: $compose_scope" ;;
|
||||
esac
|
||||
|
||||
case "$deps_mode" in
|
||||
with-deps) ;;
|
||||
no-deps) run_args+=(--no-deps) ;;
|
||||
*) fail "Unknown runtime CLI deps mode: $deps_mode" ;;
|
||||
esac
|
||||
|
||||
docker compose "${compose_args[@]}" "${run_args[@]}" openclaw-cli "$@"
|
||||
}
|
||||
|
||||
run_gateway_up() {
|
||||
local compose_scope="${1:-current}"
|
||||
shift
|
||||
|
||||
local -a compose_args
|
||||
local -a up_args=(up -d)
|
||||
|
||||
case "$compose_scope" in
|
||||
current) compose_args=("${COMPOSE_ARGS[@]}") ;;
|
||||
base) compose_args=("${BASE_COMPOSE_ARGS[@]}") ;;
|
||||
*) fail "Unknown gateway compose scope: $compose_scope" ;;
|
||||
esac
|
||||
|
||||
if [[ -n "$OFFLINE_MODE" ]]; then
|
||||
up_args+=(--pull never --no-build)
|
||||
fi
|
||||
up_args+=("$@")
|
||||
|
||||
docker compose "${compose_args[@]}" "${up_args[@]}" openclaw-gateway
|
||||
}
|
||||
|
||||
resolve_offline_sandbox_images() {
|
||||
local agents_json sandbox_tools_json
|
||||
agents_json="$(run_prestart_cli config get agents --json 2>/dev/null || true)"
|
||||
if [[ -z "$agents_json" ]]; then
|
||||
agents_json="{}"
|
||||
fi
|
||||
sandbox_tools_json="$(
|
||||
run_prestart_cli config get tools.sandbox.tools --json 2>/dev/null || true
|
||||
)"
|
||||
if [[ -z "$sandbox_tools_json" ]]; then
|
||||
sandbox_tools_json="{}"
|
||||
fi
|
||||
|
||||
printf '%s' "$agents_json" | run_prestart_gateway \
|
||||
-T --entrypoint node openclaw-gateway -e '
|
||||
const fs = require("node:fs");
|
||||
const agents = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
|
||||
const globalToolPolicy = JSON.parse(process.argv[3] || "{}");
|
||||
const defaultSandbox = agents?.defaults?.sandbox ?? {};
|
||||
const defaultDockerImage = defaultSandbox?.docker?.image ?? process.argv[1];
|
||||
const defaultBrowserImage = defaultSandbox?.browser?.image ?? process.argv[2];
|
||||
const images = new Set();
|
||||
const configuredEntries = Array.isArray(agents?.list)
|
||||
? agents.list.filter((entry) => entry !== null && typeof entry === "object")
|
||||
: [];
|
||||
const entries = configuredEntries.length > 0 ? configuredEntries : [{ sandbox: {} }];
|
||||
|
||||
const matchesBrowser = (rawPattern) => {
|
||||
const pattern = String(rawPattern ?? "").trim().toLowerCase();
|
||||
if (pattern === "group:openclaw" || pattern === "group:ui") {
|
||||
return true;
|
||||
}
|
||||
if (!pattern) {
|
||||
return false;
|
||||
}
|
||||
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
||||
return new RegExp(`^${escaped.replaceAll("*", ".*")}$`).test("browser");
|
||||
};
|
||||
const permitsBrowser = (entry) => {
|
||||
const agentPolicy = entry?.tools?.sandbox?.tools ?? {};
|
||||
const allow = Array.isArray(agentPolicy.allow)
|
||||
? agentPolicy.allow
|
||||
: Array.isArray(globalToolPolicy?.allow)
|
||||
? globalToolPolicy.allow
|
||||
: undefined;
|
||||
const alsoAllow = Array.isArray(agentPolicy.alsoAllow)
|
||||
? agentPolicy.alsoAllow
|
||||
: Array.isArray(globalToolPolicy?.alsoAllow)
|
||||
? globalToolPolicy.alsoAllow
|
||||
: undefined;
|
||||
const deny = Array.isArray(agentPolicy.deny)
|
||||
? agentPolicy.deny
|
||||
: Array.isArray(globalToolPolicy?.deny)
|
||||
? globalToolPolicy.deny
|
||||
: undefined;
|
||||
|
||||
// Browser is absent from the default allowlist and present in the default
|
||||
// denylist. Explicit allow patterns re-enable it unless an explicit deny wins.
|
||||
const explicitAllows = [...(allow ?? []), ...(alsoAllow ?? [])];
|
||||
const allowedByAllowlist = Array.isArray(allow)
|
||||
? allow.length === 0 || explicitAllows.some(matchesBrowser)
|
||||
: (alsoAllow ?? []).some(matchesBrowser);
|
||||
const denied = Array.isArray(deny)
|
||||
? deny.some(matchesBrowser)
|
||||
: !explicitAllows.some(matchesBrowser);
|
||||
return allowedByAllowlist && !denied;
|
||||
};
|
||||
|
||||
for (const entry of entries) {
|
||||
const sandbox = entry?.sandbox ?? {};
|
||||
const mode = sandbox.mode ?? "non-main";
|
||||
const backend = (
|
||||
sandbox.backend?.trim() ||
|
||||
defaultSandbox.backend?.trim() ||
|
||||
"docker"
|
||||
).toLowerCase();
|
||||
if (mode === "off" || backend !== "docker") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Setup writes defaults scope=agent. Explicit per-agent scope still wins,
|
||||
// and shared scope intentionally ignores per-agent Docker/browser overrides.
|
||||
const scope = sandbox.scope ?? "agent";
|
||||
const agentDocker = scope === "shared" ? undefined : sandbox.docker;
|
||||
images.add(`sandbox\t${agentDocker?.image ?? defaultDockerImage}`);
|
||||
|
||||
const agentBrowser = scope === "shared" ? undefined : sandbox.browser;
|
||||
const browserEnabled = agentBrowser?.enabled ?? defaultSandbox?.browser?.enabled ?? false;
|
||||
if (browserEnabled && permitsBrowser(entry)) {
|
||||
images.add(`browser\t${agentBrowser?.image ?? defaultBrowserImage}`);
|
||||
}
|
||||
}
|
||||
process.stdout.write([...images].join("\n"));
|
||||
' "$DEFAULT_SANDBOX_IMAGE" "$DEFAULT_SANDBOX_BROWSER_IMAGE" "$sandbox_tools_json"
|
||||
}
|
||||
|
||||
validate_offline_sandbox_prerequisites() {
|
||||
if [[ ! -S "$DOCKER_SOCKET_PATH" ]]; then
|
||||
fail "Offline sandbox setup requires a Docker socket at $DOCKER_SOCKET_PATH."
|
||||
fi
|
||||
|
||||
local sandbox_images
|
||||
sandbox_images="$(resolve_offline_sandbox_images)"
|
||||
local -a sandbox_image_errors=()
|
||||
local image_kind sandbox_image browser_contract
|
||||
while IFS=$'\t' read -r image_kind sandbox_image; do
|
||||
[[ -n "$image_kind" ]] || continue
|
||||
case "$image_kind" in
|
||||
sandbox)
|
||||
if ! docker --host "unix://$DOCKER_SOCKET_PATH" image inspect "$sandbox_image" >/dev/null 2>&1; then
|
||||
sandbox_image_errors+=("$sandbox_image (missing)")
|
||||
fi
|
||||
;;
|
||||
browser)
|
||||
if ! browser_contract="$(
|
||||
docker --host "unix://$DOCKER_SOCKET_PATH" image inspect \
|
||||
-f '{{ index .Config.Labels "org.openclaw.sandbox-browser.contract" }}' \
|
||||
"$sandbox_image" 2>/dev/null
|
||||
)"; then
|
||||
sandbox_image_errors+=("$sandbox_image (missing)")
|
||||
elif [[ "$browser_contract" != "$SANDBOX_BROWSER_IMAGE_CONTRACT_EPOCH" ]]; then
|
||||
sandbox_image_errors+=(
|
||||
"$sandbox_image (browser contract=${browser_contract:-missing}, expected=$SANDBOX_BROWSER_IMAGE_CONTRACT_EPOCH)"
|
||||
)
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
fail "Unknown offline sandbox image kind: $image_kind"
|
||||
;;
|
||||
esac
|
||||
done <<<"$sandbox_images"
|
||||
|
||||
if [[ ${#sandbox_image_errors[@]} -gt 0 ]]; then
|
||||
echo "WARNING: offline Docker setup cannot use required sandbox images:" >&2
|
||||
local sandbox_image_error
|
||||
for sandbox_image_error in "${sandbox_image_errors[@]}"; do
|
||||
echo " - $sandbox_image_error" >&2
|
||||
done
|
||||
echo " Load them with 'docker load -i <sandbox-image.tar>' before enabling sandboxed agents." >&2
|
||||
fail "Offline sandbox prerequisites are incomplete; sandbox configuration was not changed."
|
||||
fi
|
||||
|
||||
echo "Using preloaded sandbox images:"
|
||||
while IFS=$'\t' read -r _ sandbox_image; do
|
||||
if [[ -n "$sandbox_image" ]]; then
|
||||
echo " - $sandbox_image"
|
||||
fi
|
||||
done <<<"$sandbox_images"
|
||||
|
||||
if ! run_compose_one_off --rm --entrypoint docker openclaw-gateway --version >/dev/null 2>&1; then
|
||||
fail "Offline sandbox setup requires Docker CLI in $IMAGE_NAME."
|
||||
fi
|
||||
}
|
||||
|
||||
contains_disallowed_chars() {
|
||||
local value="$1"
|
||||
[[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]]
|
||||
}
|
||||
|
||||
is_valid_timezone() {
|
||||
local value="$1"
|
||||
[[ -e "/usr/share/zoneinfo/$value" && ! -d "/usr/share/zoneinfo/$value" ]]
|
||||
}
|
||||
|
||||
validate_mount_path_value() {
|
||||
local label="$1"
|
||||
local value="$2"
|
||||
if [[ -z "$value" ]]; then
|
||||
fail "$label cannot be empty."
|
||||
fi
|
||||
if contains_disallowed_chars "$value"; then
|
||||
fail "$label contains unsupported control characters."
|
||||
fi
|
||||
}
|
||||
|
||||
validate_named_volume() {
|
||||
local value="$1"
|
||||
if [[ ! "$value" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then
|
||||
fail "OPENCLAW_HOME_VOLUME must match [A-Za-z0-9][A-Za-z0-9_.-]* when using a named volume."
|
||||
fi
|
||||
}
|
||||
|
||||
validate_mount_spec() {
|
||||
local mount="$1"
|
||||
if contains_disallowed_chars "$mount"; then
|
||||
fail "OPENCLAW_EXTRA_MOUNTS entries cannot contain control characters."
|
||||
fi
|
||||
# Keep mount specs strict to avoid YAML structure injection.
|
||||
# Expected format: source:target[:options]
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$DOCKER_SOCKET_PATH" && "${DOCKER_HOST:-}" == unix://* ]]; then
|
||||
DOCKER_SOCKET_PATH="${DOCKER_HOST#unix://}"
|
||||
fi
|
||||
if [[ -z "$DOCKER_SOCKET_PATH" ]]; then
|
||||
DOCKER_SOCKET_PATH="/var/run/docker.sock"
|
||||
fi
|
||||
if is_truthy_value "$RAW_SANDBOX_SETTING"; then
|
||||
SANDBOX_ENABLED="1"
|
||||
fi
|
||||
if is_truthy_value "$RAW_SKIP_ONBOARDING"; then
|
||||
SKIP_ONBOARDING="1"
|
||||
fi
|
||||
|
||||
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
|
||||
OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
|
||||
OPENCLAW_AUTH_PROFILE_SECRET_DIR="${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-$HOME/.openclaw-auth-profile-secrets}"
|
||||
|
||||
validate_mount_path_value "OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR"
|
||||
validate_mount_path_value "OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_WORKSPACE_DIR"
|
||||
validate_mount_path_value "OPENCLAW_AUTH_PROFILE_SECRET_DIR" "$OPENCLAW_AUTH_PROFILE_SECRET_DIR"
|
||||
if [[ -n "$HOME_VOLUME_NAME" ]]; then
|
||||
if [[ "$HOME_VOLUME_NAME" == *"/"* ]]; then
|
||||
validate_mount_path_value "OPENCLAW_HOME_VOLUME" "$HOME_VOLUME_NAME"
|
||||
else
|
||||
validate_named_volume "$HOME_VOLUME_NAME"
|
||||
fi
|
||||
fi
|
||||
if contains_disallowed_chars "$EXTRA_MOUNTS"; then
|
||||
fail "OPENCLAW_EXTRA_MOUNTS cannot contain control characters."
|
||||
fi
|
||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||
validate_mount_path_value "OPENCLAW_DOCKER_SOCKET" "$DOCKER_SOCKET_PATH"
|
||||
fi
|
||||
if [[ -n "$TIMEZONE" ]]; then
|
||||
if contains_disallowed_chars "$TIMEZONE"; then
|
||||
fail "OPENCLAW_TZ contains unsupported control characters."
|
||||
fi
|
||||
if [[ ! "$TIMEZONE" =~ ^[A-Za-z0-9/_+\-]+$ ]]; then
|
||||
fail "OPENCLAW_TZ must be a valid IANA timezone string (e.g. Asia/Shanghai)."
|
||||
fi
|
||||
if ! is_valid_timezone "$TIMEZONE"; then
|
||||
fail "OPENCLAW_TZ must match a timezone in /usr/share/zoneinfo (e.g. Asia/Shanghai)."
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$OPENCLAW_CONFIG_DIR"
|
||||
mkdir -p "$OPENCLAW_WORKSPACE_DIR"
|
||||
mkdir -p "$OPENCLAW_AUTH_PROFILE_SECRET_DIR"
|
||||
# Seed directory tree eagerly so bind mounts work even on Docker Desktop/Windows
|
||||
# where the container (even as root) cannot create new host subdirectories.
|
||||
mkdir -p "$OPENCLAW_CONFIG_DIR/identity"
|
||||
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/agent"
|
||||
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/sessions"
|
||||
|
||||
export OPENCLAW_CONFIG_DIR
|
||||
export OPENCLAW_WORKSPACE_DIR
|
||||
export OPENCLAW_AUTH_PROFILE_SECRET_DIR
|
||||
export OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
|
||||
export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}"
|
||||
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_DOCKER_BUILD_NODE_OPTIONS="${OPENCLAW_DOCKER_BUILD_NODE_OPTIONS---max-old-space-size=8192}"
|
||||
export OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB="${OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB:-}"
|
||||
export OPENCLAW_DOCKER_BUILD_SKIP_DTS="${OPENCLAW_DOCKER_BUILD_SKIP_DTS:-1}"
|
||||
export OPENCLAW_INSTALL_BROWSER="${OPENCLAW_INSTALL_BROWSER:-}"
|
||||
export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS"
|
||||
export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME"
|
||||
export OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}"
|
||||
export OPENCLAW_SANDBOX="$SANDBOX_ENABLED"
|
||||
export OPENCLAW_DOCKER_SOCKET="$DOCKER_SOCKET_PATH"
|
||||
export OPENCLAW_DOCKER_SETUP=1
|
||||
export OPENCLAW_TZ="$TIMEZONE"
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT="${OTEL_EXPORTER_OTLP_ENDPOINT:-}"
|
||||
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}"
|
||||
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:-}"
|
||||
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="${OTEL_EXPORTER_OTLP_LOGS_ENDPOINT:-}"
|
||||
export OTEL_EXPORTER_OTLP_PROTOCOL="${OTEL_EXPORTER_OTLP_PROTOCOL:-}"
|
||||
export OTEL_SERVICE_NAME="${OTEL_SERVICE_NAME:-}"
|
||||
export OTEL_SEMCONV_STABILITY_OPT_IN="${OTEL_SEMCONV_STABILITY_OPT_IN:-}"
|
||||
export OPENCLAW_OTEL_PRELOADED="${OPENCLAW_OTEL_PRELOADED:-}"
|
||||
export OPENCLAW_SKIP_ONBOARDING="$SKIP_ONBOARDING"
|
||||
|
||||
# Detect Docker socket GID for sandbox group_add.
|
||||
DOCKER_GID=""
|
||||
if [[ -n "$SANDBOX_ENABLED" && -S "$DOCKER_SOCKET_PATH" ]]; then
|
||||
DOCKER_GID="$(stat -c '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || stat -f '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || echo "")"
|
||||
fi
|
||||
export DOCKER_GID
|
||||
|
||||
if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then
|
||||
EXISTING_CONFIG_TOKEN="$(read_config_gateway_token || true)"
|
||||
if [[ -n "$EXISTING_CONFIG_TOKEN" ]]; then
|
||||
OPENCLAW_GATEWAY_TOKEN="$EXISTING_CONFIG_TOKEN"
|
||||
echo "Reusing gateway token from $OPENCLAW_CONFIG_DIR/openclaw.json"
|
||||
else
|
||||
DOTENV_GATEWAY_TOKEN="$(read_env_gateway_token "$ROOT_DIR/.env" || true)"
|
||||
if [[ -n "$DOTENV_GATEWAY_TOKEN" ]]; then
|
||||
OPENCLAW_GATEWAY_TOKEN="$DOTENV_GATEWAY_TOKEN"
|
||||
echo "Reusing gateway token from $ROOT_DIR/.env"
|
||||
elif command -v openssl >/dev/null 2>&1; then
|
||||
OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)"
|
||||
else
|
||||
OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY'
|
||||
import secrets
|
||||
print(secrets.token_hex(32))
|
||||
PY
|
||||
)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
export OPENCLAW_GATEWAY_TOKEN
|
||||
|
||||
COMPOSE_FILES=("$COMPOSE_FILE")
|
||||
COMPOSE_ARGS=()
|
||||
|
||||
write_extra_compose() {
|
||||
local home_volume="$1"
|
||||
shift
|
||||
local mount
|
||||
local gateway_home_mount
|
||||
local gateway_config_mount
|
||||
local gateway_workspace_mount
|
||||
local gateway_auth_profile_secret_mount
|
||||
|
||||
cat >"$EXTRA_COMPOSE_FILE" <<'YAML'
|
||||
services:
|
||||
openclaw-gateway:
|
||||
volumes:
|
||||
YAML
|
||||
|
||||
if [[ -n "$home_volume" ]]; then
|
||||
gateway_home_mount="${home_volume}:/home/node"
|
||||
gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw"
|
||||
gateway_workspace_mount="${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace"
|
||||
gateway_auth_profile_secret_mount="${OPENCLAW_AUTH_PROFILE_SECRET_DIR}:/home/node/.config/openclaw"
|
||||
validate_mount_spec "$gateway_home_mount"
|
||||
validate_mount_spec "$gateway_config_mount"
|
||||
validate_mount_spec "$gateway_workspace_mount"
|
||||
validate_mount_spec "$gateway_auth_profile_secret_mount"
|
||||
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' "$(quote_yaml_string "$mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||
done
|
||||
|
||||
cat >>"$EXTRA_COMPOSE_FILE" <<'YAML'
|
||||
openclaw-cli:
|
||||
volumes:
|
||||
YAML
|
||||
|
||||
if [[ -n "$home_volume" ]]; then
|
||||
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' "$(quote_yaml_string "$mount")" >>"$EXTRA_COMPOSE_FILE"
|
||||
done
|
||||
|
||||
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
|
||||
validate_named_volume "$home_volume"
|
||||
cat >>"$EXTRA_COMPOSE_FILE" <<YAML
|
||||
volumes:
|
||||
${home_volume}:
|
||||
YAML
|
||||
fi
|
||||
}
|
||||
|
||||
# When sandbox is requested, ensure Docker CLI build arg is set for local builds.
|
||||
# Docker socket mount is deferred until sandbox prerequisites are verified.
|
||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||
if [[ -z "${OPENCLAW_INSTALL_DOCKER_CLI:-}" ]]; then
|
||||
export OPENCLAW_INSTALL_DOCKER_CLI=1
|
||||
fi
|
||||
fi
|
||||
|
||||
VALID_MOUNTS=()
|
||||
if [[ -n "$EXTRA_MOUNTS" ]]; then
|
||||
IFS=',' read -r -a mounts <<<"$EXTRA_MOUNTS"
|
||||
for mount in "${mounts[@]}"; do
|
||||
mount="${mount#"${mount%%[![:space:]]*}"}"
|
||||
mount="${mount%"${mount##*[![:space:]]}"}"
|
||||
if [[ -n "$mount" ]]; then
|
||||
VALID_MOUNTS+=("$mount")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -n "$HOME_VOLUME_NAME" || ${#VALID_MOUNTS[@]} -gt 0 ]]; then
|
||||
# Bash 3.2 + nounset treats "${array[@]}" on an empty array as unbound.
|
||||
if [[ ${#VALID_MOUNTS[@]} -gt 0 ]]; then
|
||||
write_extra_compose "$HOME_VOLUME_NAME" "${VALID_MOUNTS[@]}"
|
||||
else
|
||||
write_extra_compose "$HOME_VOLUME_NAME"
|
||||
fi
|
||||
COMPOSE_FILES+=("$EXTRA_COMPOSE_FILE")
|
||||
fi
|
||||
for compose_file in "${COMPOSE_FILES[@]}"; do
|
||||
COMPOSE_ARGS+=("-f" "$compose_file")
|
||||
done
|
||||
# Keep a base compose arg set without sandbox overlay so rollback paths can
|
||||
# force a known-safe gateway service definition (no docker.sock mount).
|
||||
BASE_COMPOSE_ARGS=("${COMPOSE_ARGS[@]}")
|
||||
COMPOSE_HINT="docker compose"
|
||||
for compose_file in "${COMPOSE_FILES[@]}"; do
|
||||
COMPOSE_HINT+=" -f ${compose_file}"
|
||||
done
|
||||
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
upsert_env() {
|
||||
local file="$1"
|
||||
shift
|
||||
local -a keys=("$@")
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
# Use a delimited string instead of an associative array so the script
|
||||
# works with Bash 3.2 (macOS default) which lacks `declare -A`.
|
||||
local seen=" "
|
||||
|
||||
if [[ -f "$file" ]]; then
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
local key="${line%%=*}"
|
||||
local replaced=false
|
||||
for k in "${keys[@]}"; do
|
||||
if [[ "$key" == "$k" ]]; then
|
||||
printf '%s=%s\n' "$k" "${!k-}" >>"$tmp"
|
||||
seen="$seen$k "
|
||||
replaced=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$replaced" == false ]]; then
|
||||
printf '%s\n' "$line" >>"$tmp"
|
||||
fi
|
||||
done <"$file"
|
||||
fi
|
||||
|
||||
for k in "${keys[@]}"; do
|
||||
if [[ "$seen" != *" $k "* ]]; then
|
||||
printf '%s=%s\n' "$k" "${!k-}" >>"$tmp"
|
||||
fi
|
||||
done
|
||||
|
||||
mv "$tmp" "$file"
|
||||
}
|
||||
|
||||
upsert_env "$ENV_FILE" \
|
||||
OPENCLAW_CONFIG_DIR \
|
||||
OPENCLAW_WORKSPACE_DIR \
|
||||
OPENCLAW_AUTH_PROFILE_SECRET_DIR \
|
||||
OPENCLAW_GATEWAY_PORT \
|
||||
OPENCLAW_BRIDGE_PORT \
|
||||
OPENCLAW_GATEWAY_BIND \
|
||||
OPENCLAW_DISABLE_BONJOUR \
|
||||
OPENCLAW_GATEWAY_TOKEN \
|
||||
OPENCLAW_IMAGE \
|
||||
OPENCLAW_EXTRA_MOUNTS \
|
||||
OPENCLAW_HOME_VOLUME \
|
||||
OPENCLAW_IMAGE_APT_PACKAGES \
|
||||
OPENCLAW_IMAGE_PIP_PACKAGES \
|
||||
OPENCLAW_EXTENSIONS \
|
||||
OPENCLAW_DOCKER_BUILD_NODE_OPTIONS \
|
||||
OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB \
|
||||
OPENCLAW_DOCKER_BUILD_SKIP_DTS \
|
||||
OPENCLAW_INSTALL_BROWSER \
|
||||
OPENCLAW_SANDBOX \
|
||||
OPENCLAW_DOCKER_SOCKET \
|
||||
DOCKER_GID \
|
||||
OPENCLAW_INSTALL_DOCKER_CLI \
|
||||
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS \
|
||||
OPENCLAW_TZ \
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT \
|
||||
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT \
|
||||
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT \
|
||||
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT \
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL \
|
||||
OTEL_SERVICE_NAME \
|
||||
OTEL_SEMCONV_STABILITY_OPT_IN \
|
||||
OPENCLAW_OTEL_PRELOADED \
|
||||
OPENCLAW_SKIP_ONBOARDING
|
||||
|
||||
if [[ -n "$OFFLINE_MODE" ]]; then
|
||||
require_local_docker_image "$IMAGE_NAME"
|
||||
echo "==> Using preloaded Docker image: $IMAGE_NAME"
|
||||
elif [[ "$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_DOCKER_BUILD_NODE_OPTIONS=${OPENCLAW_DOCKER_BUILD_NODE_OPTIONS}" \
|
||||
--build-arg "OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=${OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB}" \
|
||||
--build-arg "OPENCLAW_DOCKER_BUILD_SKIP_DTS=${OPENCLAW_DOCKER_BUILD_SKIP_DTS}" \
|
||||
--build-arg "OPENCLAW_INSTALL_BROWSER=${OPENCLAW_INSTALL_BROWSER}" \
|
||||
--build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \
|
||||
-t "$IMAGE_NAME" \
|
||||
-f "$ROOT_DIR/Dockerfile" \
|
||||
"$ROOT_DIR"
|
||||
else
|
||||
echo "==> Pulling Docker image: $IMAGE_NAME"
|
||||
if ! run_docker_pull "$IMAGE_NAME"; then
|
||||
echo "ERROR: Failed to pull image $IMAGE_NAME. Please check the image name and your access permissions." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure bind-mounted data directories are writable by the container's `node`
|
||||
# user (uid 1000). Host-created dirs inherit the host user's uid which may
|
||||
# differ, causing EACCES when the container tries to mkdir/write.
|
||||
# Running a brief root container to chown is the portable Docker idiom --
|
||||
# it works regardless of the host uid and doesn't require host-side root.
|
||||
echo ""
|
||||
echo "==> Fixing data-directory permissions"
|
||||
# Use -xdev to restrict chown to the config-dir mount only — without it,
|
||||
# the recursive chown would cross into the workspace bind mount and rewrite
|
||||
# ownership of all user project files on Linux hosts.
|
||||
# After fixing the config dir, only the OpenClaw metadata subdirectory
|
||||
# (.openclaw/) inside the workspace gets chowned, not the user's project files.
|
||||
run_prestart_gateway --user root --entrypoint sh openclaw-gateway -c \
|
||||
'find /home/node/.openclaw -xdev -exec chown node:node {} +; \
|
||||
chown node:node /home/node/.config; \
|
||||
find /home/node/.config/openclaw -xdev -exec chown node:node {} +; \
|
||||
[ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true'
|
||||
|
||||
echo ""
|
||||
if [[ -n "$SKIP_ONBOARDING" ]]; then
|
||||
echo "==> Skipping onboarding (OPENCLAW_SKIP_ONBOARDING is set)"
|
||||
else
|
||||
echo "==> Onboarding (interactive)"
|
||||
echo "Docker setup pins Gateway mode to local."
|
||||
echo "Gateway runtime bind comes from OPENCLAW_GATEWAY_BIND (default: lan)."
|
||||
echo "Current runtime bind: $OPENCLAW_GATEWAY_BIND"
|
||||
if is_truthy_value "$OPENCLAW_DISABLE_BONJOUR"; then
|
||||
echo "Bonjour/mDNS advertising: force disabled (OPENCLAW_DISABLE_BONJOUR=$OPENCLAW_DISABLE_BONJOUR)."
|
||||
elif [[ -z "$OPENCLAW_DISABLE_BONJOUR" ]]; then
|
||||
echo "Bonjour/mDNS advertising: auto (disabled inside the Gateway container unless explicitly enabled)."
|
||||
else
|
||||
echo "Bonjour/mDNS advertising: explicitly enabled (OPENCLAW_DISABLE_BONJOUR=$OPENCLAW_DISABLE_BONJOUR)."
|
||||
fi
|
||||
echo "Gateway token: stored in Docker environment/config (not printed)."
|
||||
echo "Tailscale exposure: Off (use host-level tailnet/Tailscale setup separately)."
|
||||
echo "Install Gateway daemon: No (managed by Docker Compose)"
|
||||
echo ""
|
||||
run_prestart_cli onboard \
|
||||
--mode local \
|
||||
--no-install-daemon \
|
||||
--gateway-auth token \
|
||||
--gateway-token-ref-env OPENCLAW_GATEWAY_TOKEN \
|
||||
--skip-ui \
|
||||
--suppress-gateway-token-output
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> Docker gateway defaults"
|
||||
sync_gateway_config
|
||||
|
||||
echo ""
|
||||
echo "==> Provider setup (optional)"
|
||||
echo "WhatsApp (QR):"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels login"
|
||||
echo "Telegram (bot token):"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel telegram --token <token>"
|
||||
echo "Discord (bot token):"
|
||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel discord --token <token>"
|
||||
echo "Docs: https://docs.openclaw.ai/channels"
|
||||
|
||||
if [[ -n "$SANDBOX_ENABLED" && -n "$OFFLINE_MODE" ]]; then
|
||||
echo ""
|
||||
echo "==> Sandbox preflight"
|
||||
validate_offline_sandbox_prerequisites
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> Starting gateway"
|
||||
run_gateway_up current
|
||||
|
||||
# --- Sandbox setup (opt-in via OPENCLAW_SANDBOX=1) ---
|
||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||
echo ""
|
||||
echo "==> Sandbox setup"
|
||||
|
||||
sandbox_dockerfile="$ROOT_DIR/scripts/docker/sandbox/Dockerfile"
|
||||
if [[ -z "$OFFLINE_MODE" && ! -S "$DOCKER_SOCKET_PATH" ]]; then
|
||||
echo "WARNING: OPENCLAW_SANDBOX enabled but Docker socket not found at $DOCKER_SOCKET_PATH." >&2
|
||||
echo " Sandbox requires Docker socket access. Skipping sandbox setup." >&2
|
||||
SANDBOX_ENABLED=""
|
||||
fi
|
||||
|
||||
if [[ -n "$SANDBOX_ENABLED" && -z "$OFFLINE_MODE" && -f "$sandbox_dockerfile" ]]; then
|
||||
echo "Building sandbox image: $DEFAULT_SANDBOX_IMAGE"
|
||||
run_docker_build \
|
||||
-t "$DEFAULT_SANDBOX_IMAGE" \
|
||||
-f "$sandbox_dockerfile" \
|
||||
"$ROOT_DIR"
|
||||
elif [[ -n "$SANDBOX_ENABLED" && -z "$OFFLINE_MODE" ]]; then
|
||||
echo "WARNING: sandbox Dockerfile not found at $sandbox_dockerfile" >&2
|
||||
echo " Sandbox config will be applied but no sandbox image will be built." >&2
|
||||
echo " Agent exec may fail if the configured sandbox image does not exist." >&2
|
||||
fi
|
||||
|
||||
# Defense-in-depth: verify Docker CLI in the running image before enabling
|
||||
# sandbox. This avoids claiming sandbox is enabled when the image cannot
|
||||
# launch sandbox containers.
|
||||
if [[ -n "$SANDBOX_ENABLED" && -z "$OFFLINE_MODE" ]] &&
|
||||
! run_compose_one_off --rm --entrypoint docker openclaw-gateway --version >/dev/null 2>&1; then
|
||||
echo "WARNING: Docker CLI not found inside the container image." >&2
|
||||
echo " Sandbox requires Docker CLI. Rebuild with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1" >&2
|
||||
echo " or use a local build (OPENCLAW_IMAGE=openclaw:local). Skipping sandbox setup." >&2
|
||||
SANDBOX_ENABLED=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Apply sandbox config only if prerequisites are met.
|
||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||
# Mount Docker socket via a dedicated compose overlay. This overlay is
|
||||
# created only after sandbox prerequisites pass, so the socket is never
|
||||
# exposed when sandbox cannot actually run.
|
||||
SANDBOX_COMPOSE_FILE="$ROOT_DIR/docker-compose.sandbox.yml"
|
||||
cat >"$SANDBOX_COMPOSE_FILE" <<YAML
|
||||
services:
|
||||
openclaw-gateway:
|
||||
volumes:
|
||||
- $(quote_yaml_string "${DOCKER_SOCKET_PATH}:/var/run/docker.sock")
|
||||
YAML
|
||||
if [[ -n "${DOCKER_GID:-}" ]]; then
|
||||
cat >>"$SANDBOX_COMPOSE_FILE" <<YAML
|
||||
group_add:
|
||||
- "${DOCKER_GID}"
|
||||
YAML
|
||||
fi
|
||||
COMPOSE_ARGS+=("-f" "$SANDBOX_COMPOSE_FILE")
|
||||
echo "==> Sandbox: added Docker socket mount"
|
||||
fi
|
||||
|
||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||
# Enable sandbox in OpenClaw config.
|
||||
sandbox_config_ok=true
|
||||
if ! run_runtime_cli current no-deps \
|
||||
config set agents.defaults.sandbox.mode "non-main" >/dev/null; then
|
||||
echo "WARNING: Failed to set agents.defaults.sandbox.mode" >&2
|
||||
sandbox_config_ok=false
|
||||
fi
|
||||
if ! run_runtime_cli current no-deps \
|
||||
config set agents.defaults.sandbox.scope "agent" >/dev/null; then
|
||||
echo "WARNING: Failed to set agents.defaults.sandbox.scope" >&2
|
||||
sandbox_config_ok=false
|
||||
fi
|
||||
if ! run_runtime_cli current no-deps \
|
||||
config set agents.defaults.sandbox.workspaceAccess "none" >/dev/null; then
|
||||
echo "WARNING: Failed to set agents.defaults.sandbox.workspaceAccess" >&2
|
||||
sandbox_config_ok=false
|
||||
fi
|
||||
|
||||
if [[ "$sandbox_config_ok" == true ]]; then
|
||||
echo "Sandbox enabled: mode=non-main, scope=agent, workspaceAccess=none"
|
||||
echo "Docs: https://docs.openclaw.ai/gateway/sandboxing"
|
||||
# Restart gateway with sandbox compose overlay to pick up socket mount + config.
|
||||
run_gateway_up current
|
||||
else
|
||||
echo "WARNING: Sandbox config was partially applied. Check errors above." >&2
|
||||
echo " Skipping gateway restart to avoid exposing Docker socket without a full sandbox policy." >&2
|
||||
if ! run_runtime_cli base no-deps \
|
||||
config set agents.defaults.sandbox.mode "off" >/dev/null; then
|
||||
echo "WARNING: Failed to roll back agents.defaults.sandbox.mode to off" >&2
|
||||
else
|
||||
echo "Sandbox mode rolled back to off due to partial sandbox config failure."
|
||||
fi
|
||||
if [[ -n "${SANDBOX_COMPOSE_FILE:-}" ]]; then
|
||||
rm -f "$SANDBOX_COMPOSE_FILE"
|
||||
fi
|
||||
# Ensure gateway service definition is reset without sandbox overlay mount.
|
||||
run_gateway_up base --force-recreate
|
||||
fi
|
||||
else
|
||||
# Keep reruns deterministic: if sandbox is not active for this run, reset
|
||||
# persisted sandbox mode so future execs do not require docker.sock by stale
|
||||
# config alone.
|
||||
if ! run_runtime_cli current with-deps \
|
||||
config set agents.defaults.sandbox.mode "off" >/dev/null; then
|
||||
echo "WARNING: Failed to reset agents.defaults.sandbox.mode to off" >&2
|
||||
fi
|
||||
if [[ -f "$ROOT_DIR/docker-compose.sandbox.yml" ]]; then
|
||||
rm -f "$ROOT_DIR/docker-compose.sandbox.yml"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Gateway running with host port mapping."
|
||||
echo "Access from tailnet devices via the host's tailnet IP."
|
||||
echo "Config: $OPENCLAW_CONFIG_DIR"
|
||||
echo "Workspace: $OPENCLAW_WORKSPACE_DIR"
|
||||
echo "Token: stored in Docker environment/config (not printed)."
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " ${COMPOSE_HINT} logs -f openclaw-gateway"
|
||||
echo " ${COMPOSE_HINT} exec openclaw-gateway sh -lc 'node dist/index.js health --token \"\$OPENCLAW_GATEWAY_TOKEN\"'"
|
||||
Reference in New Issue
Block a user