molecule-core/workspace-template/entrypoint.sh
Hongming Wang cb47e89aa8 fix(workspace): recursive chown when /workspace bind mount is root-owned (#13)
On Docker Desktop (macOS/Windows), host-path bind mounts often appear
root-owned inside the container. The previous entrypoint only chowned
/workspace top-level, so agents (uid 1000) still couldn't write to
/workspace/repo/* — git clone, pip install, and file edits failed with
EACCES and fell back to /tmp. Detect the root-owned-contents case by
sampling the first entry; if it's root-owned, recursively chown the
tree. On normal Linux Docker with matching uids this is a no-op, so the
fast-startup path is preserved for the common case.

Part B of the issue (private-repo initial_prompt clone) was addressed
by PR #20.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:29:30 -07:00

85 lines
4.1 KiB
Bash

#!/bin/bash
# No set -e — individual commands handle their own errors gracefully
# ──────────────────────────────────────────────────────────
# Volume ownership fix (runs as root)
# ──────────────────────────────────────────────────────────
# Docker creates volume contents as root. The agent process runs as UID 1000
# and needs to write to /configs (CLAUDE.md, skills, plugins) and /workspace
# (cloned repos, scratch files). Fix ownership once at startup so every
# future file operation works without per-file chown hacks.
if [ "$(id -u)" = "0" ]; then
# Fix /configs recursively (plugins, CLAUDE.md, skills — small directory)
chown -R agent:agent /configs 2>/dev/null
# /workspace handling:
# - Always fix the top-level dir so agent can create files in it.
# - If the contents are root-owned (common on Docker Desktop / Windows
# bind mounts where host uid maps to 0 inside the container), do a
# full recursive chown — otherwise git clone, pip install, and file
# writes under /workspace fail with EACCES (issue #13). On normal
# Linux Docker with matching uids this branch is skipped, so we keep
# the fast startup for the common case.
chown agent:agent /workspace 2>/dev/null
if [ -d /workspace ]; then
# Sample the first entry inside /workspace; if it's root-owned assume
# the whole tree is a root-owned bind mount and recursively chown.
first_entry=$(find /workspace -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)
if [ -n "$first_entry" ] && [ "$(stat -c '%u' "$first_entry" 2>/dev/null)" = "0" ]; then
echo "[entrypoint] /workspace contents are root-owned — chowning recursively to agent (uid 1000)"
chown -R agent:agent /workspace 2>/dev/null
fi
fi
# Re-exec this script as the agent user via gosu (clean PID 1 handoff)
exec gosu agent "$0" "$@"
fi
# ──────────────────────────────────────────────────────────
# Everything below runs as the agent user (UID 1000)
# ──────────────────────────────────────────────────────────
# Ensure user-installed packages are in PATH
export PATH="$HOME/.local/bin:$PATH"
# Determine runtime from config.yaml
RUNTIME=$(python3 -c "
import yaml
from pathlib import Path
cfg_path = Path('/configs/config.yaml')
if cfg_path.exists():
cfg = yaml.safe_load(cfg_path.read_text()) or {}
print(cfg.get('runtime', 'langgraph'))
else:
print('langgraph')
" 2>/dev/null || echo "langgraph")
# Normalize runtime name for directory lookup (claude-code -> claude_code)
ADAPTER_DIR=$(echo "$RUNTIME" | tr '-' '_')
echo "=== Molecule AI Workspace ==="
echo "Runtime: $RUNTIME"
# Install adapter-specific Python requirements (skip if already pre-installed in image)
REQ_FILE="/app/adapters/${ADAPTER_DIR}/requirements.txt"
if [ -f "$REQ_FILE" ]; then
if grep -q '^[^#]' "$REQ_FILE" 2>/dev/null; then
# Check if first package is already installed
FIRST_PKG=$(grep '^[^#]' "$REQ_FILE" | head -1 | sed 's/[>=<].*//')
if python3 -c "import importlib; importlib.import_module('${FIRST_PKG//-/_}')" 2>/dev/null; then
echo "Adapter deps already installed (${FIRST_PKG})"
else
echo "Installing Python adapter dependencies..."
pip install --no-cache-dir --user -q -r "$REQ_FILE" 2>&1 | tail -3
fi
fi
fi
# Install adapter-specific npm packages (for Node.js-based runtimes like OpenClaw)
NPM_FILE="/app/adapters/${ADAPTER_DIR}/package.json"
if [ -f "$NPM_FILE" ]; then
echo "Installing npm adapter dependencies..."
cd "/app/adapters/${ADAPTER_DIR}" && npm install --production 2>&1 | tail -3
cd /app
fi
exec python3 main.py