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:
34
scripts/docker/cleanup-smoke/Dockerfile
Normal file
34
scripts/docker/cleanup-smoke/Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf
|
||||
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
|
||||
RUN --mount=type=cache,id=openclaw-cleanup-smoke-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-cleanup-smoke-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
bash \
|
||||
ca-certificates \
|
||||
git
|
||||
|
||||
WORKDIR /repo
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
||||
COPY openclaw.mjs ./
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY packages ./packages
|
||||
COPY extensions ./extensions
|
||||
COPY patches ./patches
|
||||
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/prepare-git-hooks.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
|
||||
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
|
||||
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||
corepack enable \
|
||||
&& if ! pnpm install --frozen-lockfile >/tmp/openclaw-cleanup-pnpm-install.log 2>&1; then \
|
||||
cat /tmp/openclaw-cleanup-pnpm-install.log; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
COPY . .
|
||||
COPY --chmod=755 scripts/docker/cleanup-smoke/run.sh /usr/local/bin/openclaw-cleanup-smoke
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/openclaw-cleanup-smoke"]
|
||||
94
scripts/docker/cleanup-smoke/run.sh
Executable file
94
scripts/docker/cleanup-smoke/run.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd /repo
|
||||
|
||||
export OPENCLAW_STATE_DIR="/tmp/openclaw-test"
|
||||
export OPENCLAW_CONFIG_PATH="${OPENCLAW_STATE_DIR}/openclaw.json"
|
||||
|
||||
read_positive_int_env() {
|
||||
local name="${1:?missing environment variable name}"
|
||||
local fallback="${2:?missing fallback value}"
|
||||
local value="${!name-}"
|
||||
if [ -z "${!name+x}" ]; then
|
||||
value="$fallback"
|
||||
fi
|
||||
if [[ ! "$value" =~ ^[0-9]+$ ]] || (( 10#$value < 1 )); then
|
||||
echo "invalid $name: $value" >&2
|
||||
return 2
|
||||
fi
|
||||
printf '%s\n' "$((10#$value))"
|
||||
}
|
||||
|
||||
print_log_tail() {
|
||||
local log_file="$1"
|
||||
local max_bytes
|
||||
max_bytes="$(read_positive_int_env OPENCLAW_CLEANUP_SMOKE_LOG_PRINT_BYTES 65536)" || return $?
|
||||
if [ ! -f "$log_file" ]; then
|
||||
return 0
|
||||
fi
|
||||
local log_bytes
|
||||
log_bytes="$(wc -c <"$log_file" 2>/dev/null || echo 0)"
|
||||
log_bytes="${log_bytes//[[:space:]]/}"
|
||||
if ! [[ "$log_bytes" =~ ^[0-9]+$ ]]; then
|
||||
log_bytes="0"
|
||||
fi
|
||||
if [ "$log_bytes" -le "$max_bytes" ]; then
|
||||
cat "$log_file"
|
||||
return 0
|
||||
fi
|
||||
echo "--- ${log_file} truncated: showing last ${max_bytes} of ${log_bytes} bytes ---"
|
||||
tail -c "$max_bytes" "$log_file"
|
||||
}
|
||||
|
||||
read_positive_int_env OPENCLAW_CLEANUP_SMOKE_LOG_PRINT_BYTES 65536 >/dev/null
|
||||
|
||||
ensure_cleanup_smoke_node_options() {
|
||||
local current="${NODE_OPTIONS:-}"
|
||||
case " $current " in
|
||||
*" --max-old-space-size="* | *" --max-old-space-size "* | *" --max_old_space_size="* | *" --max_old_space_size "*)
|
||||
;;
|
||||
*)
|
||||
current="${current:+$current }--max-old-space-size=8192"
|
||||
;;
|
||||
esac
|
||||
export NODE_OPTIONS="$current"
|
||||
}
|
||||
ensure_cleanup_smoke_node_options
|
||||
|
||||
echo "==> Build"
|
||||
if ! pnpm build >/tmp/openclaw-cleanup-build.log 2>&1; then
|
||||
print_log_tail /tmp/openclaw-cleanup-build.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Seed state"
|
||||
mkdir -p "${OPENCLAW_STATE_DIR}/credentials"
|
||||
mkdir -p "${OPENCLAW_STATE_DIR}/agents/main/sessions"
|
||||
echo '{}' >"${OPENCLAW_CONFIG_PATH}"
|
||||
echo 'creds' >"${OPENCLAW_STATE_DIR}/credentials/marker.txt"
|
||||
echo 'session' >"${OPENCLAW_STATE_DIR}/agents/main/sessions/sessions.json"
|
||||
|
||||
echo "==> Reset (config+creds+sessions)"
|
||||
if ! pnpm openclaw reset --scope config+creds+sessions --yes --non-interactive >/tmp/openclaw-cleanup-reset.log 2>&1; then
|
||||
print_log_tail /tmp/openclaw-cleanup-reset.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test ! -f "${OPENCLAW_CONFIG_PATH}"
|
||||
test ! -d "${OPENCLAW_STATE_DIR}/credentials"
|
||||
test ! -d "${OPENCLAW_STATE_DIR}/agents/main/sessions"
|
||||
|
||||
echo "==> Recreate minimal config"
|
||||
mkdir -p "${OPENCLAW_STATE_DIR}/credentials"
|
||||
echo '{}' >"${OPENCLAW_CONFIG_PATH}"
|
||||
|
||||
echo "==> Uninstall (state only)"
|
||||
if ! pnpm openclaw uninstall --state --yes --non-interactive >/tmp/openclaw-cleanup-uninstall.log 2>&1; then
|
||||
print_log_tail /tmp/openclaw-cleanup-uninstall.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test ! -d "${OPENCLAW_STATE_DIR}"
|
||||
|
||||
echo "OK"
|
||||
53
scripts/docker/install-sh-common/cli-verify.sh
Normal file
53
scripts/docker/install-sh-common/cli-verify.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./version-parse.sh
|
||||
source "$SCRIPT_DIR/version-parse.sh"
|
||||
|
||||
verify_installed_cli() {
|
||||
local package_name="$1"
|
||||
local expected_version="$2"
|
||||
local cli_name="$package_name"
|
||||
local cmd_path=""
|
||||
local entry_path=""
|
||||
local npm_root=""
|
||||
local installed_version=""
|
||||
|
||||
cmd_path="$(command -v "$cli_name" || true)"
|
||||
if [[ -z "$cmd_path" && -x "$HOME/.npm-global/bin/$package_name" ]]; then
|
||||
cmd_path="$HOME/.npm-global/bin/$package_name"
|
||||
fi
|
||||
|
||||
if [[ -z "$cmd_path" ]]; then
|
||||
npm_root="$(quiet_npm root -g 2>/dev/null || true)"
|
||||
if [[ -n "$npm_root" && -f "$npm_root/$package_name/dist/entry.js" ]]; then
|
||||
entry_path="$npm_root/$package_name/dist/entry.js"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$cmd_path" && -z "$entry_path" ]]; then
|
||||
echo "ERROR: $package_name is not on PATH" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "$cmd_path" ]]; then
|
||||
installed_version="$("$cmd_path" --version 2>/dev/null | head -n 1 | tr -d '\r')"
|
||||
else
|
||||
installed_version="$(node "$entry_path" --version 2>/dev/null | head -n 1 | tr -d '\r')"
|
||||
fi
|
||||
|
||||
installed_version="$(extract_openclaw_semver "$installed_version")"
|
||||
|
||||
echo "cli=$cli_name installed=$installed_version expected=$expected_version"
|
||||
if [[ "$installed_version" != "$expected_version" ]]; then
|
||||
echo "ERROR: expected ${cli_name}@${expected_version}, got ${cli_name}@${installed_version}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "==> Sanity: CLI runs"
|
||||
if [[ -n "$cmd_path" ]]; then
|
||||
"$cmd_path" --help >/dev/null
|
||||
else
|
||||
node "$entry_path" --help >/dev/null
|
||||
fi
|
||||
}
|
||||
20
scripts/docker/install-sh-common/version-parse.sh
Normal file
20
scripts/docker/install-sh-common/version-parse.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
extract_openclaw_semver() {
|
||||
local raw="${1:-}"
|
||||
raw="${raw//$'\r'/}"
|
||||
if [[ "$raw" =~ v?([0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?(\+[0-9A-Za-z.-]+)?) ]]; then
|
||||
printf '%s' "${BASH_REMATCH[1]}"
|
||||
fi
|
||||
}
|
||||
|
||||
quiet_npm() {
|
||||
npm \
|
||||
--loglevel=error \
|
||||
--logs-max=0 \
|
||||
--no-update-notifier \
|
||||
--no-fund \
|
||||
--no-audit \
|
||||
--no-progress \
|
||||
"$@"
|
||||
}
|
||||
20
scripts/docker/install-sh-e2e/Dockerfile
Normal file
20
scripts/docker/install-sh-e2e/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf
|
||||
|
||||
RUN --mount=type=cache,id=openclaw-install-sh-e2e-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-install-sh-e2e-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git
|
||||
|
||||
COPY install-sh-common/version-parse.sh /usr/local/install-sh-common/version-parse.sh
|
||||
COPY --chmod=755 install-sh-e2e/run.sh /usr/local/bin/openclaw-install-e2e
|
||||
|
||||
RUN useradd --create-home --shell /bin/bash appuser
|
||||
USER appuser
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/openclaw-install-e2e"]
|
||||
1049
scripts/docker/install-sh-e2e/run.sh
Executable file
1049
scripts/docker/install-sh-e2e/run.sh
Executable file
File diff suppressed because it is too large
Load Diff
48
scripts/docker/install-sh-nonroot/Dockerfile
Normal file
48
scripts/docker/install-sh-nonroot/Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM ubuntu:24.04@sha256:cd1dba651b3080c3686ecf4e3c4220f026b521fb76978881737d24f200828b2b
|
||||
|
||||
# Smoke images are pinned and short-lived, so skip distro upgrades here and
|
||||
# spend the time budget on installer coverage instead.
|
||||
RUN --mount=type=cache,id=openclaw-install-sh-nonroot-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-install-sh-nonroot-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
set -eux; \
|
||||
for attempt in 1 2 3; do \
|
||||
if apt-get update -o Acquire::Retries=3; then break; fi; \
|
||||
echo "apt-get update failed (attempt ${attempt})" >&2; \
|
||||
if [ "${attempt}" -eq 3 ]; then exit 1; fi; \
|
||||
sleep 3; \
|
||||
done; \
|
||||
apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
g++ \
|
||||
make \
|
||||
python3 \
|
||||
sudo
|
||||
|
||||
# Preinstall the supported Node runtime in a cacheable build layer so the
|
||||
# non-root smoke covers user-local npm prefixing and missing git without paying
|
||||
# the full NodeSource bootstrap cost on every container run.
|
||||
RUN --mount=type=cache,id=openclaw-install-sh-nonroot-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-install-sh-nonroot-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
set -eux; \
|
||||
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -; \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends nodejs
|
||||
|
||||
RUN useradd -m -s /bin/bash app \
|
||||
&& echo "app ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/app
|
||||
|
||||
USER app
|
||||
WORKDIR /home/app
|
||||
|
||||
ENV NPM_CONFIG_FUND=false
|
||||
ENV NPM_CONFIG_AUDIT=false
|
||||
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
|
||||
|
||||
COPY install-sh-common/cli-verify.sh /usr/local/install-sh-common/cli-verify.sh
|
||||
COPY install-sh-common/version-parse.sh /usr/local/install-sh-common/version-parse.sh
|
||||
COPY --chmod=755 install-sh-nonroot/run.sh /usr/local/bin/openclaw-install-nonroot
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/openclaw-install-nonroot"]
|
||||
55
scripts/docker/install-sh-nonroot/run.sh
Normal file
55
scripts/docker/install-sh-nonroot/run.sh
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_URL="${OPENCLAW_INSTALL_URL:-https://openclaw.bot/install.sh}"
|
||||
DEFAULT_PACKAGE="openclaw"
|
||||
PACKAGE_NAME="${OPENCLAW_INSTALL_PACKAGE:-$DEFAULT_PACKAGE}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# shellcheck source=../install-sh-common/cli-verify.sh
|
||||
source "$SCRIPT_DIR/../install-sh-common/cli-verify.sh"
|
||||
|
||||
echo "==> Pre-flight: ensure git absent"
|
||||
if command -v git >/dev/null; then
|
||||
echo "git is present unexpectedly" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Pre-flight: ensure supported Node is already present"
|
||||
node -e '
|
||||
const version = process.versions.node.split(".").map(Number);
|
||||
const ok =
|
||||
version.length >= 2 &&
|
||||
(version[0] > 22 || (version[0] === 22 && version[1] >= 19));
|
||||
if (!ok) {
|
||||
process.stderr.write(`unsupported node ${process.versions.node}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
require("node:sqlite");
|
||||
} catch {
|
||||
process.stderr.write(`unsupported node ${process.versions.node}: missing node:sqlite\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
'
|
||||
command -v npm >/dev/null
|
||||
|
||||
echo "==> Run installer (non-root user)"
|
||||
curl -fsSL "$INSTALL_URL" | bash
|
||||
|
||||
# Ensure PATH picks up user npm prefix
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
|
||||
echo "==> Verify git installed"
|
||||
command -v git >/dev/null
|
||||
|
||||
EXPECTED_VERSION="${OPENCLAW_INSTALL_EXPECT_VERSION:-}"
|
||||
if [[ -n "$EXPECTED_VERSION" ]]; then
|
||||
LATEST_VERSION="$EXPECTED_VERSION"
|
||||
else
|
||||
LATEST_VERSION="$(quiet_npm view "$PACKAGE_NAME" version)"
|
||||
fi
|
||||
echo "==> Verify CLI installed"
|
||||
verify_installed_cli "$PACKAGE_NAME" "$LATEST_VERSION"
|
||||
|
||||
echo "OK"
|
||||
30
scripts/docker/install-sh-smoke/Dockerfile
Normal file
30
scripts/docker/install-sh-smoke/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf
|
||||
|
||||
# Smoke images are pinned and short-lived, so skip distro upgrades here and
|
||||
# spend the time budget on installer coverage instead.
|
||||
RUN --mount=type=cache,id=openclaw-install-sh-smoke-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-install-sh-smoke-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
set -eux; \
|
||||
for attempt in 1 2 3; do \
|
||||
if apt-get update -o Acquire::Retries=3; then break; fi; \
|
||||
echo "apt-get update failed (attempt ${attempt})" >&2; \
|
||||
if [ "${attempt}" -eq 3 ]; then exit 1; fi; \
|
||||
sleep 3; \
|
||||
done; \
|
||||
apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
g++ \
|
||||
make \
|
||||
python3 \
|
||||
sudo
|
||||
|
||||
COPY install-sh-common/cli-verify.sh /usr/local/install-sh-common/cli-verify.sh
|
||||
COPY install-sh-common/version-parse.sh /usr/local/install-sh-common/version-parse.sh
|
||||
COPY --chmod=755 install-sh-smoke/run.sh /usr/local/bin/openclaw-install-smoke
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/openclaw-install-smoke"]
|
||||
608
scripts/docker/install-sh-smoke/run.sh
Executable file
608
scripts/docker/install-sh-smoke/run.sh
Executable file
@@ -0,0 +1,608 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
read_positive_int_env() {
|
||||
local name="${1:?missing environment variable name}"
|
||||
local fallback="${2:?missing fallback value}"
|
||||
local value="${!name-}"
|
||||
if [[ -z "${!name+x}" ]]; then
|
||||
value="$fallback"
|
||||
fi
|
||||
if [[ ! "$value" =~ ^[0-9]+$ ]] || (( 10#$value < 1 )); then
|
||||
echo "invalid $name: $value" >&2
|
||||
return 2
|
||||
fi
|
||||
printf "%s\n" "$((10#$value))"
|
||||
}
|
||||
|
||||
read_nonnegative_int_env() {
|
||||
local name="${1:?missing environment variable name}"
|
||||
local fallback="${2:?missing fallback value}"
|
||||
local value="${!name-}"
|
||||
if [[ -z "${!name+x}" ]]; then
|
||||
value="$fallback"
|
||||
fi
|
||||
if [[ ! "$value" =~ ^[0-9]+$ ]]; then
|
||||
echo "invalid $name: $value" >&2
|
||||
return 2
|
||||
fi
|
||||
printf "%s\n" "$((10#$value))"
|
||||
}
|
||||
|
||||
INSTALL_URL="${OPENCLAW_INSTALL_URL:-https://openclaw.bot/install.sh}"
|
||||
SMOKE_MODE="${OPENCLAW_INSTALL_SMOKE_MODE:-install}"
|
||||
SMOKE_PREVIOUS_VERSION="${OPENCLAW_INSTALL_SMOKE_PREVIOUS:-}"
|
||||
SKIP_PREVIOUS="${OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS:-0}"
|
||||
DEFAULT_PACKAGE="openclaw"
|
||||
PACKAGE_NAME="${OPENCLAW_INSTALL_PACKAGE:-$DEFAULT_PACKAGE}"
|
||||
FRESH_VERSION="${OPENCLAW_INSTALL_FRESH_VERSION:-}"
|
||||
FRESH_TAG_URL="${OPENCLAW_INSTALL_FRESH_TAG_URL:-}"
|
||||
UPDATE_BASELINE_VERSION="${OPENCLAW_INSTALL_UPDATE_BASELINE:-latest}"
|
||||
UPDATE_BASELINE_TAG_URL="${OPENCLAW_INSTALL_UPDATE_BASELINE_TAG_URL:-}"
|
||||
UPDATE_EXPECT_VERSION="${OPENCLAW_INSTALL_UPDATE_EXPECT_VERSION:-}"
|
||||
UPDATE_TAG_URL="${OPENCLAW_INSTALL_UPDATE_TAG_URL:-}"
|
||||
SELF_UPDATE_WARNING_FIXED_VERSION="${OPENCLAW_INSTALL_SELF_UPDATE_WARNING_FIXED_VERSION:-2026.5.25}"
|
||||
FRESHNESS_VERSION="${OPENCLAW_INSTALL_FRESHNESS_VERSION:-latest}"
|
||||
# npm min-release-age is days; 10000 keeps the control failure independent of normal release cadence.
|
||||
FRESHNESS_MIN_RELEASE_AGE="${OPENCLAW_INSTALL_FRESHNESS_MIN_RELEASE_AGE:-10000}"
|
||||
FRESHNESS_NPM_VERSION="${OPENCLAW_INSTALL_FRESHNESS_NPM_VERSION:-11.14.1}"
|
||||
HEARTBEAT_INTERVAL="$(read_nonnegative_int_env OPENCLAW_INSTALL_SMOKE_HEARTBEAT_INTERVAL 60)"
|
||||
INSTALL_COMMAND_TIMEOUT="$(read_positive_int_env OPENCLAW_INSTALL_SMOKE_COMMAND_TIMEOUT 900)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# shellcheck source=../install-sh-common/cli-verify.sh
|
||||
source "$SCRIPT_DIR/../install-sh-common/cli-verify.sh"
|
||||
|
||||
emit_status() {
|
||||
if [[ -w /dev/tty ]]; then
|
||||
printf "%s\n" "$*" >/dev/tty
|
||||
else
|
||||
printf "%s\n" "$*" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
global_package_root() {
|
||||
local npm_root
|
||||
npm_root="$(quiet_npm root -g 2>/dev/null || true)"
|
||||
if [[ -n "$npm_root" ]]; then
|
||||
printf "%s/%s" "$npm_root" "$PACKAGE_NAME"
|
||||
fi
|
||||
}
|
||||
|
||||
describe_installed_package() {
|
||||
local root="$1"
|
||||
local files="missing"
|
||||
local size="missing"
|
||||
local version="missing"
|
||||
if [[ -d "$root" ]]; then
|
||||
files="$(find "$root" -type f 2>/dev/null | wc -l | tr -d " ")"
|
||||
size="$(du -sh "$root" 2>/dev/null | cut -f1 || true)"
|
||||
version="$(
|
||||
node -e '
|
||||
try {
|
||||
process.stdout.write(String(require(`${process.argv[1]}/package.json`).version ?? "missing"));
|
||||
} catch {
|
||||
process.stdout.write("missing");
|
||||
}
|
||||
' "$root"
|
||||
)"
|
||||
fi
|
||||
printf "version=%s size=%s files=%s root=%s" "$version" "$size" "$files" "$root"
|
||||
}
|
||||
|
||||
print_install_audit() {
|
||||
local label="$1"
|
||||
local root
|
||||
root="$(global_package_root)"
|
||||
if [[ -n "$root" ]]; then
|
||||
echo "==> Install audit (${label}): $(describe_installed_package "$root")"
|
||||
fi
|
||||
}
|
||||
|
||||
run_with_heartbeat() {
|
||||
local label="$1"
|
||||
shift
|
||||
local interval="$HEARTBEAT_INTERVAL"
|
||||
if [[ "$interval" == "0" ]]; then
|
||||
"$@"
|
||||
return
|
||||
fi
|
||||
|
||||
local start
|
||||
local command_pid
|
||||
local heartbeat_pid
|
||||
local status
|
||||
start="$(date +%s)"
|
||||
set +e
|
||||
"$@" &
|
||||
command_pid=$!
|
||||
(
|
||||
while true; do
|
||||
sleep "$interval"
|
||||
kill -0 "$command_pid" >/dev/null 2>&1 || exit 0
|
||||
local now
|
||||
local elapsed
|
||||
local root
|
||||
now="$(date +%s)"
|
||||
elapsed=$((now - start))
|
||||
root="$(global_package_root)"
|
||||
if [[ -n "$root" ]]; then
|
||||
emit_status "==> Still running (${label}, ${elapsed}s): $(describe_installed_package "$root")"
|
||||
else
|
||||
emit_status "==> Still running (${label}, ${elapsed}s)"
|
||||
fi
|
||||
done
|
||||
) &
|
||||
heartbeat_pid=$!
|
||||
wait "$command_pid"
|
||||
status=$?
|
||||
kill "$heartbeat_pid" >/dev/null 2>&1 || true
|
||||
wait "$heartbeat_pid" >/dev/null 2>&1 || true
|
||||
set -e
|
||||
return "$status"
|
||||
}
|
||||
|
||||
is_self_swapped_package_process_exit() {
|
||||
local stderr="$1"
|
||||
[[ "$stderr" == *"[openclaw] Failed to start CLI:"* ]] &&
|
||||
[[ "$stderr" == *"ERR_MODULE_NOT_FOUND"* ]] &&
|
||||
[[ "$stderr" == *"/node_modules/openclaw/dist/"* ]]
|
||||
}
|
||||
|
||||
is_version_before() {
|
||||
local candidate="$1"
|
||||
local floor="$2"
|
||||
node - "$candidate" "$floor" <<'NODE'
|
||||
const [, , candidate, floor] = process.argv;
|
||||
function parse(version) {
|
||||
const [core, prerelease = ""] = String(version).split("-", 2);
|
||||
return {
|
||||
core: core.split(".").map((part) => Number.parseInt(part, 10) || 0),
|
||||
prerelease: prerelease ? prerelease.split(".") : [],
|
||||
};
|
||||
}
|
||||
function comparePrerelease(left, right) {
|
||||
if (left.length === 0 && right.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (left.length === 0) {
|
||||
return 1;
|
||||
}
|
||||
if (right.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
||||
const l = left[index];
|
||||
const r = right[index];
|
||||
if (l === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (r === undefined) {
|
||||
return 1;
|
||||
}
|
||||
const ln = Number.parseInt(l, 10);
|
||||
const rn = Number.parseInt(r, 10);
|
||||
const lNumeric = String(ln) === l;
|
||||
const rNumeric = String(rn) === r;
|
||||
if (lNumeric && rNumeric && ln !== rn) {
|
||||
return ln < rn ? -1 : 1;
|
||||
}
|
||||
if (lNumeric !== rNumeric) {
|
||||
return lNumeric ? -1 : 1;
|
||||
}
|
||||
if (l !== r) {
|
||||
return l < r ? -1 : 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const left = parse(candidate);
|
||||
const right = parse(floor);
|
||||
for (let index = 0; index < Math.max(left.core.length, right.core.length); index += 1) {
|
||||
const l = left.core[index] ?? 0;
|
||||
const r = right.core[index] ?? 0;
|
||||
if (l < r) {
|
||||
process.exit(0);
|
||||
}
|
||||
if (l > r) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const prereleaseOrder = comparePrerelease(left.prerelease, right.prerelease);
|
||||
process.exit(prereleaseOrder < 0 ? 0 : 1);
|
||||
NODE
|
||||
}
|
||||
|
||||
allow_legacy_update_warning() {
|
||||
[[ "${OPENCLAW_INSTALL_ALLOW_LEGACY_UPDATE_WARNING:-0}" == "1" ]] && return 0
|
||||
is_version_before "$UPDATE_BASELINE_VERSION" "$SELF_UPDATE_WARNING_FIXED_VERSION"
|
||||
}
|
||||
|
||||
npm_install_global() {
|
||||
local label="$1"
|
||||
shift
|
||||
run_with_heartbeat "$label" \
|
||||
timeout --kill-after=30s "${INSTALL_COMMAND_TIMEOUT}s" \
|
||||
npm \
|
||||
--loglevel=error \
|
||||
--logs-max=0 \
|
||||
--no-update-notifier \
|
||||
--no-fund \
|
||||
--no-audit \
|
||||
--no-progress \
|
||||
install -g "$@"
|
||||
}
|
||||
|
||||
resolve_update_baseline_version() {
|
||||
if [[ -n "$UPDATE_BASELINE_TAG_URL" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local resolved_version
|
||||
resolved_version="$(quiet_npm view "${PACKAGE_NAME}@${UPDATE_BASELINE_VERSION}" version 2>/dev/null || true)"
|
||||
if [[ -z "$resolved_version" ]]; then
|
||||
echo "ERROR: failed to resolve ${PACKAGE_NAME}@${UPDATE_BASELINE_VERSION}" >&2
|
||||
return 1
|
||||
fi
|
||||
UPDATE_BASELINE_VERSION="$resolved_version"
|
||||
}
|
||||
|
||||
run_installer_for_package_spec() {
|
||||
local install_url="$1"
|
||||
local package_spec="$2"
|
||||
|
||||
timeout --kill-after=30s "${INSTALL_COMMAND_TIMEOUT}s" \
|
||||
bash -c "curl -fsSL \"\$1\" | bash -s -- --install-method npm --version \"\$2\" --no-prompt --no-onboard" \
|
||||
_ "$install_url" "$package_spec"
|
||||
}
|
||||
|
||||
run_install_smoke() {
|
||||
if [[ -n "$FRESH_VERSION" && -n "$FRESH_TAG_URL" ]]; then
|
||||
echo "package=$PACKAGE_NAME latest=$FRESH_VERSION source=$FRESH_TAG_URL"
|
||||
echo "==> Run official installer one-liner for latest release tarball"
|
||||
OPENCLAW_NO_ONBOARD=1 OPENCLAW_NO_PROMPT=1 \
|
||||
run_with_heartbeat "installer latest release tarball" \
|
||||
run_installer_for_package_spec "$INSTALL_URL" "$FRESH_TAG_URL"
|
||||
print_install_audit "fresh install"
|
||||
|
||||
echo "==> Verify installed version"
|
||||
if [[ -n "${OPENCLAW_INSTALL_LATEST_OUT:-}" ]]; then
|
||||
# Non-root installer smoke uses the public install script path, which
|
||||
# resolves npm "latest" rather than this host-served candidate tarball.
|
||||
local latest_npm_version
|
||||
latest_npm_version="$(quiet_npm view "$PACKAGE_NAME" version 2>/dev/null || true)"
|
||||
if [[ -n "$latest_npm_version" ]]; then
|
||||
printf "%s" "$latest_npm_version" > "${OPENCLAW_INSTALL_LATEST_OUT:-}"
|
||||
else
|
||||
printf "%s" "$FRESH_VERSION" > "${OPENCLAW_INSTALL_LATEST_OUT:-}"
|
||||
fi
|
||||
fi
|
||||
verify_installed_cli "$PACKAGE_NAME" "$FRESH_VERSION"
|
||||
|
||||
echo "OK"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "==> Resolve npm versions"
|
||||
if [[ "$SKIP_PREVIOUS" == "1" ]]; then
|
||||
LATEST_VERSION="$(quiet_npm view "$PACKAGE_NAME" version)"
|
||||
PREVIOUS_VERSION="$LATEST_VERSION"
|
||||
elif [[ -n "$SMOKE_PREVIOUS_VERSION" ]]; then
|
||||
LATEST_VERSION="$(quiet_npm view "$PACKAGE_NAME" version)"
|
||||
PREVIOUS_VERSION="$SMOKE_PREVIOUS_VERSION"
|
||||
else
|
||||
LATEST_VERSION="$(quiet_npm view "$PACKAGE_NAME" dist-tags.latest)"
|
||||
VERSIONS_JSON="$(quiet_npm view "$PACKAGE_NAME" versions --json)"
|
||||
PREVIOUS_VERSION="$(LATEST_VERSION="$LATEST_VERSION" VERSIONS_JSON="$VERSIONS_JSON" node - <<'NODE'
|
||||
const latest = String(process.env.LATEST_VERSION || "");
|
||||
const raw = process.env.VERSIONS_JSON || "[]";
|
||||
let versions;
|
||||
try {
|
||||
versions = JSON.parse(raw);
|
||||
} catch {
|
||||
versions = raw ? [raw] : [];
|
||||
}
|
||||
if (!Array.isArray(versions)) {
|
||||
versions = [versions];
|
||||
}
|
||||
if (versions.length === 0 || latest.length === 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
const latestIndex = versions.lastIndexOf(latest);
|
||||
if (latestIndex <= 0) {
|
||||
process.stdout.write(latest);
|
||||
process.exit(0);
|
||||
}
|
||||
process.stdout.write(String(versions[latestIndex - 1] ?? latest));
|
||||
NODE
|
||||
)"
|
||||
fi
|
||||
|
||||
echo "package=$PACKAGE_NAME latest=$LATEST_VERSION previous=$PREVIOUS_VERSION"
|
||||
|
||||
if [[ "$SKIP_PREVIOUS" == "1" ]]; then
|
||||
echo "==> Skip preinstall previous (OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS=1)"
|
||||
else
|
||||
echo "==> Preinstall previous (forces installer upgrade path)"
|
||||
npm_install_global "preinstall previous release" "${PACKAGE_NAME}@${PREVIOUS_VERSION}"
|
||||
print_install_audit "previous install"
|
||||
fi
|
||||
|
||||
echo "==> Run official installer one-liner"
|
||||
curl -fsSL "$INSTALL_URL" | bash -s -- --no-prompt
|
||||
|
||||
echo "==> Verify installed version"
|
||||
if [[ -n "${OPENCLAW_INSTALL_LATEST_OUT:-}" ]]; then
|
||||
printf "%s" "$LATEST_VERSION" > "${OPENCLAW_INSTALL_LATEST_OUT:-}"
|
||||
fi
|
||||
verify_installed_cli "$PACKAGE_NAME" "$LATEST_VERSION"
|
||||
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
run_update_smoke() {
|
||||
if [[ -z "$UPDATE_EXPECT_VERSION" ]]; then
|
||||
echo "ERROR: OPENCLAW_INSTALL_UPDATE_EXPECT_VERSION is required for update mode" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ -z "$UPDATE_TAG_URL" ]]; then
|
||||
echo "ERROR: OPENCLAW_INSTALL_UPDATE_TAG_URL is required for update mode" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
resolve_update_baseline_version
|
||||
|
||||
echo "package=$PACKAGE_NAME baseline=$UPDATE_BASELINE_VERSION target=$UPDATE_EXPECT_VERSION"
|
||||
echo "==> Install baseline release"
|
||||
if [[ -n "$UPDATE_BASELINE_TAG_URL" ]]; then
|
||||
npm_install_global "install baseline release" --omit=optional "$UPDATE_BASELINE_TAG_URL"
|
||||
else
|
||||
npm_install_global "install baseline release" --omit=optional "${PACKAGE_NAME}@${UPDATE_BASELINE_VERSION}"
|
||||
fi
|
||||
print_install_audit "baseline install"
|
||||
verify_installed_cli "$PACKAGE_NAME" "$UPDATE_BASELINE_VERSION"
|
||||
|
||||
echo "==> Run openclaw update from host-served tgz"
|
||||
local update_status
|
||||
local update_stderr_file
|
||||
local update_stderr
|
||||
local update_env=(
|
||||
env
|
||||
npm_config_omit=optional
|
||||
NPM_CONFIG_OMIT=optional
|
||||
OPENCLAW_ALLOW_ROOT=1
|
||||
)
|
||||
if allow_legacy_update_warning; then
|
||||
update_env+=(OPENCLAW_UPDATE_IN_PROGRESS=1)
|
||||
fi
|
||||
update_stderr_file="$(mktemp)"
|
||||
set +e
|
||||
UPDATE_JSON="$(
|
||||
run_with_heartbeat "openclaw update" \
|
||||
"${update_env[@]}" \
|
||||
openclaw update --tag "$UPDATE_TAG_URL" --yes --json 2>"$update_stderr_file"
|
||||
)"
|
||||
update_status=$?
|
||||
set -e
|
||||
update_stderr="$(cat "$update_stderr_file")"
|
||||
rm -f "$update_stderr_file"
|
||||
printf "%s\n" "$UPDATE_JSON"
|
||||
if [[ -n "$update_stderr" ]]; then
|
||||
printf "%s\n" "$update_stderr" >&2
|
||||
fi
|
||||
if [[ "$update_stderr" == *"config was written by version"* ]] && allow_legacy_update_warning; then
|
||||
echo "WARN: legacy baseline emitted a self-update version-skew warning; fixed baselines must not" >&2
|
||||
elif [[ "$update_stderr" == *"config was written by version"* ]]; then
|
||||
echo "ERROR: openclaw update emitted a self-update version-skew warning" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ "$update_status" -ne 0 ]]; then
|
||||
if is_self_swapped_package_process_exit "$update_stderr"; then
|
||||
echo "WARN: legacy updater process exited after self-swap; validating update JSON and installed CLI" >&2
|
||||
else
|
||||
echo "ERROR: openclaw update failed with exit code $update_status" >&2
|
||||
return "$update_status"
|
||||
fi
|
||||
fi
|
||||
|
||||
UPDATE_JSON="$UPDATE_JSON" \
|
||||
UPDATE_EXPECT_VERSION="$UPDATE_EXPECT_VERSION" \
|
||||
UPDATE_BASELINE_VERSION="$UPDATE_BASELINE_VERSION" \
|
||||
UPDATE_TAG_URL="$UPDATE_TAG_URL" \
|
||||
node - <<'NODE'
|
||||
function parseFirstJsonObject(raw) {
|
||||
const start = raw.indexOf("{");
|
||||
if (start < 0) {
|
||||
throw new Error("missing update JSON object");
|
||||
}
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
for (let index = start; index < raw.length; index += 1) {
|
||||
const char = raw[index];
|
||||
if (inString) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (char === "\\") {
|
||||
escaped = true;
|
||||
} else if (char === '"') {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (char === '"') {
|
||||
inString = true;
|
||||
} else if (char === "{") {
|
||||
depth += 1;
|
||||
} else if (char === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return JSON.parse(raw.slice(start, index + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("unterminated update JSON object");
|
||||
}
|
||||
|
||||
const payload = parseFirstJsonObject(process.env.UPDATE_JSON || "{}");
|
||||
const expectedVersion = String(process.env.UPDATE_EXPECT_VERSION || "");
|
||||
const baselineVersion = String(process.env.UPDATE_BASELINE_VERSION || "");
|
||||
const expectedUrl = String(process.env.UPDATE_TAG_URL || "");
|
||||
if (payload.status !== "ok") {
|
||||
throw new Error(`expected update status ok, got ${JSON.stringify(payload.status)}`);
|
||||
}
|
||||
if ((payload.before?.version ?? null) !== baselineVersion) {
|
||||
throw new Error(
|
||||
`expected before.version ${baselineVersion}, got ${JSON.stringify(payload.before?.version)}`,
|
||||
);
|
||||
}
|
||||
if ((payload.after?.version ?? null) !== expectedVersion) {
|
||||
throw new Error(
|
||||
`expected after.version ${expectedVersion}, got ${JSON.stringify(payload.after?.version)}`,
|
||||
);
|
||||
}
|
||||
if (payload.reason != null) {
|
||||
throw new Error(`expected no failure reason, got ${JSON.stringify(payload.reason)}`);
|
||||
}
|
||||
const steps = Array.isArray(payload.steps) ? payload.steps : [];
|
||||
const updateStep = steps.find((step) => step?.name === "global update");
|
||||
if (!updateStep) {
|
||||
throw new Error("missing global update step in update JSON");
|
||||
}
|
||||
if (Number(updateStep.exitCode ?? 1) !== 0) {
|
||||
throw new Error(`global update step failed: ${JSON.stringify(updateStep)}`);
|
||||
}
|
||||
if (typeof updateStep.command !== "string" || !updateStep.command.includes(expectedUrl)) {
|
||||
throw new Error(`global update step missing expected tgz URL: ${JSON.stringify(updateStep)}`);
|
||||
}
|
||||
NODE
|
||||
|
||||
echo "==> Verify updated version"
|
||||
print_install_audit "updated install"
|
||||
verify_installed_cli "$PACKAGE_NAME" "$UPDATE_EXPECT_VERSION"
|
||||
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
run_npm_global_smoke() {
|
||||
if [[ -z "$UPDATE_EXPECT_VERSION" ]]; then
|
||||
echo "ERROR: OPENCLAW_INSTALL_UPDATE_EXPECT_VERSION is required for npm-global mode" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ -z "$UPDATE_TAG_URL" ]]; then
|
||||
echo "ERROR: OPENCLAW_INSTALL_UPDATE_TAG_URL is required for npm-global mode" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
resolve_update_baseline_version
|
||||
|
||||
echo "package=$PACKAGE_NAME baseline=$UPDATE_BASELINE_VERSION target=$UPDATE_EXPECT_VERSION"
|
||||
echo "==> Direct npm global install candidate"
|
||||
npm_install_global "direct npm global install candidate" "$UPDATE_TAG_URL"
|
||||
print_install_audit "direct npm fresh install"
|
||||
verify_installed_cli "$PACKAGE_NAME" "$UPDATE_EXPECT_VERSION"
|
||||
|
||||
echo "==> Direct npm global install baseline"
|
||||
if [[ -n "$UPDATE_BASELINE_TAG_URL" ]]; then
|
||||
npm_install_global "direct npm global install baseline" "$UPDATE_BASELINE_TAG_URL"
|
||||
else
|
||||
npm_install_global "direct npm global install baseline" "${PACKAGE_NAME}@${UPDATE_BASELINE_VERSION}"
|
||||
fi
|
||||
print_install_audit "direct npm baseline install"
|
||||
verify_installed_cli "$PACKAGE_NAME" "$UPDATE_BASELINE_VERSION"
|
||||
|
||||
echo "==> Direct npm global update candidate"
|
||||
npm_install_global "direct npm global update candidate" "$UPDATE_TAG_URL"
|
||||
print_install_audit "direct npm updated install"
|
||||
verify_installed_cli "$PACKAGE_NAME" "$UPDATE_EXPECT_VERSION"
|
||||
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
run_freshness_smoke() {
|
||||
local freshness_spec="${PACKAGE_NAME}@${FRESHNESS_VERSION}"
|
||||
local expected_version
|
||||
local current_npm_version
|
||||
local policy_home
|
||||
local plain_stdout_file
|
||||
local plain_stderr_file
|
||||
local plain_status
|
||||
policy_home="$(mktemp -d)"
|
||||
plain_stdout_file="$(mktemp)"
|
||||
plain_stderr_file="$(mktemp)"
|
||||
printf "min-release-age=%s\n" "$FRESHNESS_MIN_RELEASE_AGE" >"${policy_home}/.npmrc"
|
||||
|
||||
current_npm_version="$(npm --version 2>/dev/null || true)"
|
||||
if [[ "$current_npm_version" != "$FRESHNESS_NPM_VERSION" ]]; then
|
||||
echo "==> Install npm with min-release-age support: npm@$FRESHNESS_NPM_VERSION"
|
||||
npm_install_global "install npm freshness-capable release" "npm@${FRESHNESS_NPM_VERSION}"
|
||||
fi
|
||||
|
||||
expected_version="$(quiet_npm view "$freshness_spec" version 2>/dev/null || true)"
|
||||
if [[ -z "$expected_version" ]]; then
|
||||
echo "ERROR: failed to resolve $freshness_spec" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "package=$PACKAGE_NAME version=$FRESHNESS_VERSION resolved=$expected_version npm=$(npm --version) min_release_age=$FRESHNESS_MIN_RELEASE_AGE"
|
||||
echo "==> Verify user npm freshness policy blocks plain npm install"
|
||||
set +e
|
||||
HOME="$policy_home" NPM_CONFIG_USERCONFIG="${policy_home}/.npmrc" \
|
||||
timeout --kill-after=30s "${INSTALL_COMMAND_TIMEOUT}s" \
|
||||
npm \
|
||||
--loglevel=error \
|
||||
--logs-max=0 \
|
||||
--no-update-notifier \
|
||||
--no-fund \
|
||||
--no-audit \
|
||||
--no-progress \
|
||||
install -g "$freshness_spec" \
|
||||
>"$plain_stdout_file" 2>"$plain_stderr_file"
|
||||
plain_status=$?
|
||||
set -e
|
||||
if [[ "$plain_status" -eq 0 ]]; then
|
||||
echo "ERROR: plain npm install unexpectedly succeeded under min-release-age policy" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! grep -Eiq "No matching version|No versions available|ETARGET|ENOVERSIONS|notarget|min-release-age|minimum release age|before" \
|
||||
"$plain_stdout_file" "$plain_stderr_file"; then
|
||||
echo "ERROR: plain npm install failed without expected freshness evidence" >&2
|
||||
cat "$plain_stdout_file"
|
||||
cat "$plain_stderr_file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "==> Run installer with same npm freshness policy"
|
||||
env \
|
||||
HOME="$policy_home" \
|
||||
NPM_CONFIG_USERCONFIG="${policy_home}/.npmrc" \
|
||||
OPENCLAW_NO_ONBOARD=1 \
|
||||
OPENCLAW_NO_PROMPT=1 \
|
||||
bash -c 'curl -fsSL "$1" | bash -s -- --install-method npm --version "$2" --no-prompt --no-onboard' \
|
||||
_ "$INSTALL_URL" "$FRESHNESS_VERSION"
|
||||
|
||||
echo "==> Verify installed version"
|
||||
print_install_audit "freshness install"
|
||||
verify_installed_cli "$PACKAGE_NAME" "$expected_version"
|
||||
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
case "$SMOKE_MODE" in
|
||||
install)
|
||||
run_install_smoke
|
||||
;;
|
||||
update)
|
||||
run_update_smoke
|
||||
;;
|
||||
npm-global)
|
||||
run_npm_global_smoke
|
||||
;;
|
||||
freshness)
|
||||
run_freshness_smoke
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unsupported OPENCLAW_INSTALL_SMOKE_MODE=$SMOKE_MODE" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
21
scripts/docker/sandbox/Dockerfile
Normal file
21
scripts/docker/sandbox/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM debian:bookworm-slim@sha256:f9c6a2fd2ddbc23e336b6257a5245e31f996953ef06cd13a59fa0a1df2d5c252
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
jq \
|
||||
python3 \
|
||||
ripgrep
|
||||
|
||||
RUN useradd --create-home --shell /bin/bash sandbox
|
||||
USER sandbox
|
||||
WORKDIR /home/sandbox
|
||||
|
||||
CMD ["sleep", "infinity"]
|
||||
36
scripts/docker/sandbox/Dockerfile.browser
Normal file
36
scripts/docker/sandbox/Dockerfile.browser
Normal file
@@ -0,0 +1,36 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM debian:bookworm-slim@sha256:f9c6a2fd2ddbc23e336b6257a5245e31f996953ef06cd13a59fa0a1df2d5c252
|
||||
|
||||
LABEL org.openclaw.sandbox-browser.contract="2026-05-12-cdp-relay-auth"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
bash \
|
||||
ca-certificates \
|
||||
chromium \
|
||||
curl \
|
||||
fonts-liberation \
|
||||
fonts-noto-cjk \
|
||||
fonts-noto-color-emoji \
|
||||
git \
|
||||
jq \
|
||||
novnc \
|
||||
python3 \
|
||||
websockify \
|
||||
x11vnc \
|
||||
xvfb
|
||||
|
||||
COPY --chmod=755 scripts/sandbox-browser-entrypoint.sh /usr/local/bin/openclaw-sandbox-browser
|
||||
|
||||
RUN useradd --create-home --shell /bin/bash sandbox
|
||||
USER sandbox
|
||||
WORKDIR /home/sandbox
|
||||
|
||||
EXPOSE 9222 5900 6080
|
||||
|
||||
CMD ["openclaw-sandbox-browser"]
|
||||
60
scripts/docker/sandbox/Dockerfile.common
Normal file
60
scripts/docker/sandbox/Dockerfile.common
Normal file
@@ -0,0 +1,60 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
ARG BASE_IMAGE=openclaw-sandbox:bookworm-slim
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
USER root
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ARG PACKAGES="curl wget jq coreutils grep python3 git ca-certificates golang-go rustc cargo unzip pkg-config libasound2-dev build-essential file"
|
||||
ARG INSTALL_NODE=1
|
||||
ARG NODE_MAJOR=24
|
||||
ARG INSTALL_PNPM=1
|
||||
ARG INSTALL_BUN=1
|
||||
ARG BUN_INSTALL_DIR=/opt/bun
|
||||
ARG INSTALL_BREW=1
|
||||
ARG BREW_INSTALL_DIR=/home/linuxbrew/.linuxbrew
|
||||
ARG FINAL_USER=sandbox
|
||||
|
||||
ENV BUN_INSTALL=${BUN_INSTALL_DIR}
|
||||
ENV HOMEBREW_PREFIX=${BREW_INSTALL_DIR}
|
||||
ENV HOMEBREW_CELLAR=${BREW_INSTALL_DIR}/Cellar
|
||||
ENV HOMEBREW_REPOSITORY=${BREW_INSTALL_DIR}/Homebrew
|
||||
ENV PATH=${BUN_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/sbin:${PATH}
|
||||
|
||||
RUN --mount=type=cache,id=openclaw-sandbox-common-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-sandbox-common-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ${PACKAGES}
|
||||
|
||||
RUN --mount=type=cache,id=openclaw-sandbox-common-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=openclaw-sandbox-common-apt-lists,target=/var/lib/apt,sharing=locked \
|
||||
if [ "${INSTALL_NODE}" = "1" ]; then \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends ca-certificates curl; \
|
||||
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -; \
|
||||
apt-get install -y --no-install-recommends nodejs; \
|
||||
node --version; \
|
||||
npm --version; \
|
||||
fi
|
||||
|
||||
RUN if [ "${INSTALL_PNPM}" = "1" ]; then npm install -g pnpm && pnpm --version; fi
|
||||
|
||||
RUN if [ "${INSTALL_BUN}" = "1" ]; then \
|
||||
curl -fsSL https://bun.sh/install | bash; \
|
||||
ln -sf "${BUN_INSTALL_DIR}/bin/bun" /usr/local/bin/bun; \
|
||||
fi
|
||||
|
||||
RUN if [ "${INSTALL_BREW}" = "1" ]; then \
|
||||
if ! id -u linuxbrew >/dev/null 2>&1; then useradd -m -s /bin/bash linuxbrew; fi; \
|
||||
mkdir -p "${BREW_INSTALL_DIR}"; \
|
||||
chown -R linuxbrew:linuxbrew "$(dirname "${BREW_INSTALL_DIR}")"; \
|
||||
su - linuxbrew -c "NONINTERACTIVE=1 CI=1 /bin/bash -c '$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)'"; \
|
||||
if [ ! -e "${BREW_INSTALL_DIR}/Library" ]; then ln -s "${BREW_INSTALL_DIR}/Homebrew/Library" "${BREW_INSTALL_DIR}/Library"; fi; \
|
||||
if [ ! -x "${BREW_INSTALL_DIR}/bin/brew" ]; then echo \"brew install failed\"; exit 1; fi; \
|
||||
ln -sf "${BREW_INSTALL_DIR}/bin/brew" /usr/local/bin/brew; \
|
||||
fi
|
||||
|
||||
# Default is sandbox, but allow BASE_IMAGE overrides to select another final user.
|
||||
USER ${FINAL_USER}
|
||||
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