diff --git a/workspace-template/entrypoint.sh b/workspace-template/entrypoint.sh index 8c260ccf..e5dce4fb 100644 --- a/workspace-template/entrypoint.sh +++ b/workspace-template/entrypoint.sh @@ -1,87 +1,49 @@ -#!/bin/bash -# No set -e — individual commands handle their own errors gracefully +#!/bin/sh +# Drop privileges to the agent user before exec'ing molecule-runtime. +# claude-code refuses --dangerously-skip-permissions when running as +# root/sudo for safety. Without this entrypoint, every cron tick fails +# with `ProcessError: Command failed with exit code 1` and the agent +# logs `--dangerously-skip-permissions cannot be used with root/sudo +# privileges for security reasons`. +# +# Pattern matches the legacy monorepo workspace-template/entrypoint.sh: +# fix volume ownership as root, then re-exec via gosu as agent (uid 1000). -# ────────────────────────────────────────────────────────── -# 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) + # Configs volume is created by Docker as root; agent needs write access + # for plugin installs, memory writes, .auth_token rotation, etc. 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 + # Strip CRLF from hook scripts — Windows Docker Desktop copies host files + # with CRLF line endings even when .gitattributes says eol=lf. The \r in + # the shebang line makes python3 try to open 'script.py\r' → ENOENT → + # claude-code swallows the hook error → "(no response generated)". + # This is the permanent fix — runs at every container start. + for f in /configs/.claude/hooks/*.sh /configs/.claude/hooks/*.py; do + [ -f "$f" ] && sed -i 's/\r$//' "$f" + done + # /workspace handling — only chown when the contents are root-owned + # (typical on Docker Desktop on Windows where host uid maps to 0). + # On Linux Docker with matching uids the recursive chown is skipped + # to keep startup fast. + chown agent:agent /workspace 2>/dev/null || true 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) + # Claude Code session directory — mounted at /root/.claude/sessions by + # the platform provisioner. Symlink it into agent's home so the SDK + # finds it when running as agent. The provisioner's mount point is + # hardcoded to /root/.claude/sessions; we don't want to change the + # platform contract just for this template. + mkdir -p /home/agent/.claude + if [ -d /root/.claude/sessions ]; then + chown -R agent:agent /root/.claude /home/agent/.claude 2>/dev/null + ln -sfn /root/.claude/sessions /home/agent/.claude/sessions + fi 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") - -echo "=== Molecule AI Workspace ===" -echo "Runtime: $RUNTIME" - -# ────────────────────────────────────────────────────────── -# GitHub credential helper — issue #547 -# ────────────────────────────────────────────────────────── -# GitHub App installation tokens expire after ~60 min. The platform -# exposes GET /admin/github-installation-token (backed by the plugin's -# in-process refreshing cache) so workspaces can always get a valid -# token without restarting. -# -# Register molecule-git-token-helper.sh as the git credential helper for -# github.com. git calls it on every push/fetch; it hits the platform -# endpoint and emits a fresh token. Falls through to any existing -# credential helper (e.g. operator .env PAT) if the platform is -# unreachable. -# -# Idempotent — safe to re-run on restart. -HELPER_SCRIPT="/app/scripts/molecule-git-token-helper.sh" -if [ -f "${HELPER_SCRIPT}" ]; then - git config --global \ - "credential.https://github.com.helper" \ - "!${HELPER_SCRIPT}" 2>/dev/null || true - echo "[entrypoint] git credential helper registered (molecule-git-token-helper)" -else - echo "[entrypoint] WARNING: molecule-git-token-helper.sh not found at ${HELPER_SCRIPT} — GitHub tokens may expire after 60 min" -fi - -# NOTE: Adapter-specific deps are now pre-installed in each adapter's Docker image -# (standalone template repos). Each image installs molecule-ai-workspace-runtime -# from PyPI plus the adapter-specific requirements. No per-runtime pip install needed here. - -exec python3 main.py +# Now running as agent (uid 1000) +exec molecule-runtime "$@"