diff --git a/Dockerfile b/Dockerfile index a20f47b..b4fde02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,15 +28,28 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ && apt-get update && apt-get install -y gh \ && rm -rf /var/lib/apt/lists/* -# xurl CLI (X/Twitter API v2) -RUN XURL_TAG=$(curl -sf https://api.github.com/repos/xdevplatform/xurl/releases/latest | jq -r '.tag_name') \ - && [ "$XURL_TAG" != "null" ] && [ -n "$XURL_TAG" ] \ - && curl -fsSL "https://github.com/xdevplatform/xurl/releases/download/${XURL_TAG}/xurl_Linux_x86_64.tar.gz" \ - | tar -xz -C /usr/local/bin xurl \ - && chmod +x /usr/local/bin/xurl - -# Google Workspace CLI (gws) + agent skills -RUN npm install -g @googleworkspace/cli +# xurl CLI (X/Twitter API v2) — pinned version with SHA256 verification +ARG XURL_VERSION=1.0.3 +ARG XURL_SHA256_AMD64=34bc67bfbaf29ae121f7788fbd2491d3a8b95cb3947333ad39732e694497c182 +ARG XURL_SHA256_ARM64=3b56605e66508d7bc77c36cc711d41307b4cd76aec09111890b33f9d82975483 +RUN ARCH=$(dpkg --print-architecture) \ + && case "$ARCH" in \ + amd64) XURL_ARCH="x86_64"; XURL_SHA256="${XURL_SHA256_AMD64}" ;; \ + arm64) XURL_ARCH="arm64"; XURL_SHA256="${XURL_SHA256_ARM64}" ;; \ + *) echo "Unsupported architecture: $ARCH" && exit 1 ;; \ + esac \ + && curl -fsSL "https://github.com/xdevplatform/xurl/releases/download/v${XURL_VERSION}/xurl_Linux_${XURL_ARCH}.tar.gz" \ + -o /tmp/xurl.tar.gz \ + && echo "${XURL_SHA256} /tmp/xurl.tar.gz" | sha256sum -c - \ + && tar -xz -C /usr/local/bin -f /tmp/xurl.tar.gz xurl \ + && chmod +x /usr/local/bin/xurl \ + && rm /tmp/xurl.tar.gz + +# Google Workspace CLI + Claude CLI — pinned versions, single layer +ARG CLAUDE_CLI_VERSION=2.1.83 +RUN npm install -g \ + @googleworkspace/cli@0.22.1 \ + @anthropic-ai/claude-code@${CLAUDE_CLI_VERSION} # Agent templates (read-only source for entrypoint to copy into workspace) COPY --chown=node:node agents/ /opt/openclaw-agents/ diff --git a/init.d/01-tools.sh b/init.d/01-tools.sh index 268f466..41d8946 100755 --- a/init.d/01-tools.sh +++ b/init.d/01-tools.sh @@ -3,21 +3,16 @@ # SCRIPT_NAME and log() are provided by docker-entrypoint.sh # Runs as `node` user. Tools are installed once and persist across restarts. -# --- Claude Code CLI --- -if command -v claude &>/dev/null; then - log "Claude CLI already installed: $(claude --version 2>/dev/null || echo 'unknown')" -else - log "Installing Claude CLI..." - curl -fsSL https://claude.ai/install.sh | bash - log "Claude CLI installed: $(claude --version 2>/dev/null || echo 'unknown')" -fi +# Pinned versions (correspond to Dockerfile npm pins) +GWS_COMMIT="a52d297cdfafbc53dfed66a3721a9bbd1d50dc31" # @googleworkspace/cli@0.22.1 -# --- Google Workspace CLI skills --- +# Claude CLI is pre-installed in the Docker image; assert it is on PATH. +command -v claude >/dev/null 2>&1 || log "WARNING: Claude CLI not found in image." if npx skills list 2>/dev/null | grep -q googleworkspace; then log "GWS skills already installed." else - log "Installing Google Workspace skills..." - npx -y skills add https://github.com/googleworkspace/cli -y + log "Installing Google Workspace skills (pinned commit: ${GWS_COMMIT})..." + npx -y skills add "https://github.com/googleworkspace/cli#${GWS_COMMIT}" -y log "GWS skills installed." fi diff --git a/installer/gum.sh b/installer/gum.sh index 6e81aaf..8b7b075 100644 --- a/installer/gum.sh +++ b/installer/gum.sh @@ -5,6 +5,14 @@ GUM_MIN_MAJOR=0 GUM_MIN_MINOR=14 +# Pinned release — update SHA256 values when bumping version +GUM_PINNED_VERSION="0.17.0" +# SHA256 checksums from https://github.com/charmbracelet/gum/releases/download/v0.17.0/checksums.txt +GUM_SHA256_LINUX_X86_64="69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb" +GUM_SHA256_LINUX_ARM64="b0b9ed95cbf7c8b7073f17b9591811f5c001e33c7cfd066ca83ce8a07c576f9c" +GUM_SHA256_DARWIN_ARM64="e2a4b8596efa05821d8c58d0c1afbcd7ad1699ba69c689cc3ff23a4a99c8b237" +GUM_SHA256_DARWIN_X86_64="cd66576aeebe6cd19c771863c7e8d696e0e1d5387d1e7075666baa67c2052e53" + # gum_detect — returns 0 if gum >= 0.14 is installed, 1 otherwise gum_detect() { if ! command -v gum >/dev/null 2>&1; then @@ -86,37 +94,47 @@ gum_install() { fi fi - # Binary download fallback - echo "Downloading gum binary from GitHub releases..." - local os arch download_url tmpdir + echo "Downloading gum v${GUM_PINNED_VERSION} binary from GitHub releases..." + local os arch download_url expected_sha256 tmpdir os=$(uname -s | tr '[:upper:]' '[:lower:]') arch=$(uname -m) - case "$arch" in - x86_64) arch="x86_64" ;; - aarch64|arm64) arch="arm64" ;; + + local os_cap + os_cap=$(echo "$os" | awk '{print toupper(substr($0,1,1)) substr($0,2)}') + + case "${os}/${arch}" in + linux/x86_64) arch="x86_64"; expected_sha256="${GUM_SHA256_LINUX_X86_64}" ;; + linux/aarch64|\ + linux/arm64) arch="arm64"; expected_sha256="${GUM_SHA256_LINUX_ARM64}" ;; + darwin/arm64) arch="arm64"; expected_sha256="${GUM_SHA256_DARWIN_ARM64}" ;; + darwin/x86_64) arch="x86_64"; expected_sha256="${GUM_SHA256_DARWIN_X86_64}" ;; *) - echo "Unsupported architecture: $arch" + echo "Unsupported platform: ${os}/${arch}" return 1 ;; esac - # Query latest release from GitHub API - local version - version=$(curl -sf "https://api.github.com/repos/charmbracelet/gum/releases/latest" 2>/dev/null | \ - grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4 | tr -d 'v' || echo "") - if [ -z "$version" ]; then - # Fallback version if API is unreachable - version="0.14.5" - echo "Could not fetch latest version, trying $version..." - fi - - local os_cap - os_cap=$(echo "$os" | awk '{print toupper(substr($0,1,1)) substr($0,2)}') - - download_url="https://github.com/charmbracelet/gum/releases/download/v${version}/gum_${version}_${os_cap}_${arch}.tar.gz" + download_url="https://github.com/charmbracelet/gum/releases/download/v${GUM_PINNED_VERSION}/gum_${GUM_PINNED_VERSION}_${os_cap}_${arch}.tar.gz" tmpdir=$(mktemp -d) if curl -fsSL "$download_url" -o "${tmpdir}/gum.tar.gz" 2>/dev/null; then + # Verify integrity before extracting (tampered archive could exploit tar path traversal) + local sha_cmd + if command -v sha256sum >/dev/null 2>&1; then + sha_cmd="sha256sum" + elif command -v shasum >/dev/null 2>&1; then + sha_cmd="shasum -a 256" + else + echo "ERROR: No sha256sum or shasum found; cannot verify download." + rm -rf "$tmpdir" + return 1 + fi + if ! echo "${expected_sha256} ${tmpdir}/gum.tar.gz" | $sha_cmd -c - >/dev/null 2>&1; then + echo "ERROR: SHA256 mismatch for gum download." + rm -rf "$tmpdir" + return 1 + fi + tar -xzf "${tmpdir}/gum.tar.gz" -C "$tmpdir" 2>/dev/null local gum_bin gum_bin=$(find "$tmpdir" -name "gum" -type f | head -1)