From e38866ab61dfeb7c142aaf30a9c174d33aafd228 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 24 Mar 2026 18:53:55 +0000 Subject: [PATCH 1/3] fix: Containerize full stack with docker-compose (#618) Add docker-compose.yml generation, agent Dockerfile, and new CLI commands (up/down/logs/shell) so the full stack runs containerized. The --bare flag preserves the current bare-metal setup. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 264 +++++++++++++++++++++++++++++++++--- docker/agents/Dockerfile | 20 +++ docker/agents/entrypoint.sh | 51 +++++++ lib/env.sh | 15 +- 4 files changed, 329 insertions(+), 21 deletions(-) create mode 100644 docker/agents/Dockerfile create mode 100644 docker/agents/entrypoint.sh diff --git a/bin/disinto b/bin/disinto index f40d589..b8d5964 100755 --- a/bin/disinto +++ b/bin/disinto @@ -4,12 +4,19 @@ # # Commands: # disinto init [options] Bootstrap a new project +# disinto up Start the full stack (docker compose) +# disinto down Stop the full stack +# disinto logs [service] Tail service logs +# disinto shell Shell into the agent container # disinto status Show factory status # disinto secrets Manage encrypted secrets # # Usage: # disinto init https://github.com/user/repo # disinto init user/repo --branch main --ci-id 3 +# disinto init user/repo --bare (bare-metal, no compose) +# disinto up +# disinto down # disinto status # ============================================================================= set -euo pipefail @@ -25,6 +32,10 @@ disinto — autonomous code factory CLI Usage: disinto init [options] Bootstrap a new project + disinto up Start the full stack (docker compose) + disinto down Stop the full stack + disinto logs [service] Tail service logs + disinto shell Shell into the agent container disinto status Show factory status disinto secrets Manage encrypted secrets (.env.enc) @@ -33,6 +44,7 @@ Init options: --repo-root Local clone path (default: ~/name) --ci-id Woodpecker CI repo ID (default: 0 = no CI) --forge-url Forge base URL (default: http://localhost:3000) + --bare Skip compose generation (bare-metal setup) --yes Skip confirmation prompts EOF exit 1 @@ -136,15 +148,129 @@ write_secrets_encrypted() { FORGEJO_DATA_DIR="${HOME}/.disinto/forgejo" +# Generate docker-compose.yml in the factory root. +generate_compose() { + local forge_port="${1:-3000}" + local compose_file="${FACTORY_ROOT}/docker-compose.yml" + + cat > "$compose_file" <<'COMPOSEEOF' +# docker-compose.yml — generated by disinto init +# Brings up Forgejo, Woodpecker, and the agent runtime. + +services: + forgejo: + image: forgejo/forgejo:latest + restart: unless-stopped + volumes: + - forgejo-data:/data + environment: + FORGEJO__database__DB_TYPE: sqlite3 + FORGEJO__server__ROOT_URL: http://forgejo:3000/ + FORGEJO__server__HTTP_PORT: "3000" + FORGEJO__service__DISABLE_REGISTRATION: "true" + networks: + - disinto-net + + woodpecker: + image: woodpeckerci/woodpecker-server:latest + restart: unless-stopped + volumes: + - woodpecker-data:/var/lib/woodpecker + environment: + WOODPECKER_FORGEJO: "true" + WOODPECKER_FORGEJO_URL: http://forgejo:3000 + WOODPECKER_FORGEJO_CLIENT: ${WP_FORGEJO_CLIENT:-} + WOODPECKER_FORGEJO_SECRET: ${WP_FORGEJO_SECRET:-} + WOODPECKER_HOST: http://woodpecker:8000 + WOODPECKER_DATABASE_DRIVER: sqlite3 + WOODPECKER_DATABASE_DATASOURCE: /var/lib/woodpecker/woodpecker.sqlite + depends_on: + - forgejo + networks: + - disinto-net + + agents: + build: ./docker/agents + restart: unless-stopped + user: "1000:1000" + volumes: + - agent-data:/home/agent/data + - project-repos:/home/agent/repos + - ./:/home/agent/disinto:ro + - claude-auth:/home/agent/.claude:ro + environment: + FORGE_URL: http://forgejo:3000 + WOODPECKER_SERVER: http://woodpecker:8000 + DISINTO_CONTAINER: "1" + env_file: + - .env + depends_on: + - forgejo + - woodpecker + networks: + - disinto-net + +volumes: + forgejo-data: + woodpecker-data: + agent-data: + project-repos: + claude-auth: + +networks: + disinto-net: + driver: bridge +COMPOSEEOF + + # Patch the forgejo port mapping into the file if non-default + if [ "$forge_port" != "3000" ]; then + # Add port mapping to forgejo service so it's reachable from host during init + sed -i "/image: forgejo\/forgejo:latest/a\\ ports:\\n - \"${forge_port}:3000\"" "$compose_file" + else + sed -i "/image: forgejo\/forgejo:latest/a\\ ports:\\n - \"3000:3000\"" "$compose_file" + fi + + echo "Created: ${compose_file}" +} + +# Generate docker/agents/ files if they don't already exist. +generate_agent_docker() { + local docker_dir="${FACTORY_ROOT}/docker/agents" + mkdir -p "$docker_dir" + + if [ ! -f "${docker_dir}/Dockerfile" ]; then + echo "Warning: docker/agents/Dockerfile not found — expected in repo" >&2 + fi + if [ ! -f "${docker_dir}/entrypoint.sh" ]; then + echo "Warning: docker/agents/entrypoint.sh not found — expected in repo" >&2 + fi +} + +# Check whether compose mode is active (docker-compose.yml exists). +is_compose_mode() { + [ -f "${FACTORY_ROOT}/docker-compose.yml" ] +} + # Provision or connect to a local Forgejo instance. # Creates admin + bot users, generates API tokens, stores in .env. +# When $DISINTO_BARE is set, uses standalone docker run; otherwise uses compose. setup_forge() { local forge_url="$1" local repo_slug="$2" + local use_bare="${DISINTO_BARE:-false}" echo "" echo "── Forge setup ────────────────────────────────────────" + # Helper: run a command inside the Forgejo container + _forgejo_exec() { + if [ "$use_bare" = true ]; then + docker exec disinto-forgejo "$@" + else + docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T forgejo "$@" + fi + } + # Check if Forgejo is already running if curl -sf --max-time 5 "${forge_url}/api/v1/version" >/dev/null 2>&1; then echo "Forgejo: ${forge_url} (already running)" @@ -158,29 +284,33 @@ setup_forge() { exit 1 fi - # Create data directory - mkdir -p "${FORGEJO_DATA_DIR}" - # Extract port from forge_url local forge_port forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|') forge_port="${forge_port:-3000}" - # Start Forgejo container - if docker ps -a --format '{{.Names}}' | grep -q '^disinto-forgejo$'; then - docker start disinto-forgejo >/dev/null 2>&1 || true + if [ "$use_bare" = true ]; then + # Bare-metal mode: standalone docker run + mkdir -p "${FORGEJO_DATA_DIR}" + + if docker ps -a --format '{{.Names}}' | grep -q '^disinto-forgejo$'; then + docker start disinto-forgejo >/dev/null 2>&1 || true + else + docker run -d \ + --name disinto-forgejo \ + --restart unless-stopped \ + -p "${forge_port}:3000" \ + -p 2222:22 \ + -v "${FORGEJO_DATA_DIR}:/data" \ + -e "FORGEJO__database__DB_TYPE=sqlite3" \ + -e "FORGEJO__server__ROOT_URL=${forge_url}/" \ + -e "FORGEJO__server__HTTP_PORT=3000" \ + -e "FORGEJO__service__DISABLE_REGISTRATION=true" \ + forgejo/forgejo:latest + fi else - docker run -d \ - --name disinto-forgejo \ - --restart unless-stopped \ - -p "${forge_port}:3000" \ - -p 2222:22 \ - -v "${FORGEJO_DATA_DIR}:/data" \ - -e "FORGEJO__database__DB_TYPE=sqlite3" \ - -e "FORGEJO__server__ROOT_URL=${forge_url}/" \ - -e "FORGEJO__server__HTTP_PORT=3000" \ - -e "FORGEJO__service__DISABLE_REGISTRATION=true" \ - forgejo/forgejo:latest + # Compose mode: start Forgejo via docker compose + docker compose -f "${FACTORY_ROOT}/docker-compose.yml" up -d forgejo fi # Wait for Forgejo to become healthy @@ -206,7 +336,7 @@ setup_forge() { if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${admin_user}" >/dev/null 2>&1; then echo "Creating admin user: ${admin_user}" - docker exec disinto-forgejo forgejo admin user create \ + _forgejo_exec forgejo admin user create \ --admin \ --username "${admin_user}" \ --password "${admin_pass}" \ @@ -241,7 +371,7 @@ setup_forge() { -H "Authorization: token ${admin_token}" \ "${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then echo "Creating bot user: ${bot_user}" - docker exec disinto-forgejo forgejo admin user create \ + _forgejo_exec forgejo admin user create \ --username "${bot_user}" \ --password "${bot_pass}" \ --email "${bot_user}@disinto.local" \ @@ -748,18 +878,22 @@ disinto_init() { shift # Parse flags - local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag="" + local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag="" bare=false while [ $# -gt 0 ]; do case "$1" in --branch) branch="$2"; shift 2 ;; --repo-root) repo_root="$2"; shift 2 ;; --ci-id) ci_id="$2"; shift 2 ;; --forge-url) forge_url_flag="$2"; shift 2 ;; + --bare) bare=true; shift ;; --yes) auto_yes=true; shift ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac done + # Export bare-metal flag for setup_forge + export DISINTO_BARE="$bare" + # Extract org/repo slug local forge_repo forge_repo=$(parse_repo_slug "$repo_url") @@ -829,6 +963,15 @@ p.write_text(text) fi fi + # Generate compose files (unless --bare) + if [ "$bare" = false ]; then + local forge_port + forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|') + forge_port="${forge_port:-3000}" + generate_compose "$forge_port" + generate_agent_docker + fi + # Set up local Forgejo instance (provision if needed, create users/tokens/repo) setup_forge "$forge_url" "$forge_repo" @@ -919,11 +1062,24 @@ p.write_text(text) # Encrypt secrets if SOPS + age are available write_secrets_encrypted + # Bring up the full stack (compose mode only) + if [ "$bare" = false ] && [ -f "${FACTORY_ROOT}/docker-compose.yml" ]; then + echo "" + echo "── Starting full stack ────────────────────────────────" + docker compose -f "${FACTORY_ROOT}/docker-compose.yml" up -d + echo "Stack: running (forgejo + woodpecker + agents)" + fi + echo "" echo "Done. Project ${project_name} is ready." echo " Config: ${toml_path}" echo " Clone: ${repo_root}" echo " Forge: ${forge_url}/${forge_repo}" + if [ "$bare" = false ]; then + echo " Stack: docker compose (use 'disinto up/down/logs/shell')" + else + echo " Mode: bare-metal" + fi echo " Run 'disinto status' to verify." } @@ -1054,10 +1210,78 @@ disinto_secrets() { esac } +# ── up command ──────────────────────────────────────────────────────────────── + +disinto_up() { + local compose_file="${FACTORY_ROOT}/docker-compose.yml" + if [ ! -f "$compose_file" ]; then + echo "Error: docker-compose.yml not found" >&2 + echo " Run 'disinto init ' first (without --bare)" >&2 + exit 1 + fi + + # Decrypt secrets to temp .env if SOPS available and .env.enc exists + local tmp_env="" + local enc_file="${FACTORY_ROOT}/.env.enc" + local env_file="${FACTORY_ROOT}/.env" + if [ -f "$enc_file" ] && command -v sops &>/dev/null && [ ! -f "$env_file" ]; then + tmp_env="${env_file}" + sops -d --output-type dotenv "$enc_file" > "$tmp_env" + echo "Decrypted secrets for compose" + fi + + docker compose -f "$compose_file" up -d "$@" + echo "Stack is up" + + # Clean up temp .env + if [ -n "$tmp_env" ] && [ -f "$tmp_env" ]; then + rm -f "$tmp_env" + echo "Removed temporary .env" + fi +} + +# ── down command ────────────────────────────────────────────────────────────── + +disinto_down() { + local compose_file="${FACTORY_ROOT}/docker-compose.yml" + if [ ! -f "$compose_file" ]; then + echo "Error: docker-compose.yml not found" >&2 + exit 1 + fi + docker compose -f "$compose_file" down "$@" + echo "Stack is down" +} + +# ── logs command ────────────────────────────────────────────────────────────── + +disinto_logs() { + local compose_file="${FACTORY_ROOT}/docker-compose.yml" + if [ ! -f "$compose_file" ]; then + echo "Error: docker-compose.yml not found" >&2 + exit 1 + fi + docker compose -f "$compose_file" logs -f "$@" +} + +# ── shell command ───────────────────────────────────────────────────────────── + +disinto_shell() { + local compose_file="${FACTORY_ROOT}/docker-compose.yml" + if [ ! -f "$compose_file" ]; then + echo "Error: docker-compose.yml not found" >&2 + exit 1 + fi + docker compose -f "$compose_file" exec agents bash +} + # ── Main dispatch ──────────────────────────────────────────────────────────── case "${1:-}" in init) shift; disinto_init "$@" ;; + up) shift; disinto_up "$@" ;; + down) shift; disinto_down "$@" ;; + logs) shift; disinto_logs "$@" ;; + shell) shift; disinto_shell ;; status) shift; disinto_status "$@" ;; secrets) shift; disinto_secrets "$@" ;; -h|--help) usage ;; diff --git a/docker/agents/Dockerfile b/docker/agents/Dockerfile new file mode 100644 index 0000000..d3b9ba2 --- /dev/null +++ b/docker/agents/Dockerfile @@ -0,0 +1,20 @@ +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash curl git jq tmux cron python3 openssh-client ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Claude CLI +RUN curl -fsSL https://cli.anthropic.com/install.sh | sh \ + && mv /root/.claude/local/claude /usr/local/bin/claude || true + +# Non-root user +RUN useradd -m -u 1000 -s /bin/bash agent + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +USER agent +WORKDIR /home/agent + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/agents/entrypoint.sh b/docker/agents/entrypoint.sh new file mode 100644 index 0000000..00d5708 --- /dev/null +++ b/docker/agents/entrypoint.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +# entrypoint.sh — Start agent container with cron and stay alive +# +# Installs crontab entries from project TOMLs found in the factory +# mount, then runs cron in the background and tails the log. + +DISINTO_DIR="${HOME}/disinto" +LOGFILE="${HOME}/data/agent-entrypoint.log" +mkdir -p "${HOME}/data" + +log() { + printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" | tee -a "$LOGFILE" +} + +# Build crontab from project TOMLs +install_project_crons() { + local cron_lines="" + for toml in "${DISINTO_DIR}"/projects/*.toml; do + [ -f "$toml" ] || continue + local pname + pname=$(python3 -c " +import sys, tomllib +with open(sys.argv[1], 'rb') as f: + print(tomllib.load(f)['name']) +" "$toml" 2>/dev/null) || continue + + cron_lines="${cron_lines} +# disinto: ${pname} +2,7,12,17,22,27,32,37,42,47,52,57 * * * * ${DISINTO_DIR}/review/review-poll.sh ${toml} >/dev/null 2>&1 +4,9,14,19,24,29,34,39,44,49,54,59 * * * * ${DISINTO_DIR}/dev/dev-poll.sh ${toml} >/dev/null 2>&1 +0 0,6,12,18 * * * cd ${DISINTO_DIR} && bash gardener/gardener-run.sh ${toml} >/dev/null 2>&1" + done + + if [ -n "$cron_lines" ]; then + printf '%s\n' "$cron_lines" | crontab - + log "Installed crontab for projects" + else + log "No project TOMLs found — crontab empty" + fi +} + +log "Agent container starting" +install_project_crons + +# Keep container alive — cron runs in foreground via tail on log +# (cron daemon needs root; since we run as agent, we use a polling approach +# or the host cron can be used via docker compose exec) +log "Agent container ready — waiting for work" +exec tail -f /dev/null diff --git a/lib/env.sh b/lib/env.sh index f1b0bc8..72a8e0a 100755 --- a/lib/env.sh +++ b/lib/env.sh @@ -7,6 +7,14 @@ set -euo pipefail # Resolve script root (parent of lib/) FACTORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# Container detection: when running inside the agent container, DISINTO_CONTAINER +# is set by docker-compose.yml. Adjust paths so phase files, logs, and thread +# maps land on the persistent volume instead of /tmp (which is ephemeral). +if [ "${DISINTO_CONTAINER:-}" = "1" ]; then + DISINTO_DATA_DIR="${HOME}/data" + mkdir -p "${DISINTO_DATA_DIR}" +fi + # Load secrets: prefer .env.enc (SOPS-encrypted), fall back to plaintext .env if [ -f "$FACTORY_ROOT/.env.enc" ] && command -v sops &>/dev/null; then set -a @@ -123,7 +131,12 @@ wpdb() { # Matrix messaging helper — usage: matrix_send [thread_event_id] [context_tag] # Returns event_id on stdout. Registers threads for listener dispatch. # context_tag is stored in the thread map (e.g. issue number) for routing replies. -MATRIX_THREAD_MAP="${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" +# Thread map: use persistent data dir inside container, /tmp on bare metal +if [ "${DISINTO_CONTAINER:-}" = "1" ]; then + MATRIX_THREAD_MAP="${MATRIX_THREAD_MAP:-${DISINTO_DATA_DIR}/matrix-thread-map}" +else + MATRIX_THREAD_MAP="${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" +fi matrix_send() { [ -z "${MATRIX_TOKEN:-}" ] && return 0 local prefix="$1" msg="$2" thread_id="${3:-}" ctx_tag="${4:-}" From 0aa3890fb85030d5dcb6d4b5177cba57d13def4d Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 24 Mar 2026 20:30:44 +0000 Subject: [PATCH 2/3] fix: Start cron daemon in agent container entrypoint (#618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entrypoint installed a crontab but never started a cron daemon, leaving the container idle. Fix by running as root in the entrypoint (cron requires it), installing the crontab for the agent user via `crontab -u agent`, and starting cron in the foreground with `cron -f`. Remove `USER agent` from the Dockerfile and `user: "1000:1000"` from the compose template accordingly — cron jobs still execute as UID 1000. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 1 - docker/agents/Dockerfile | 3 ++- docker/agents/entrypoint.sh | 28 ++++++++++++++-------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/bin/disinto b/bin/disinto index b8d5964..bbe6d43 100755 --- a/bin/disinto +++ b/bin/disinto @@ -192,7 +192,6 @@ services: agents: build: ./docker/agents restart: unless-stopped - user: "1000:1000" volumes: - agent-data:/home/agent/data - project-repos:/home/agent/repos diff --git a/docker/agents/Dockerfile b/docker/agents/Dockerfile index d3b9ba2..2dc8a22 100644 --- a/docker/agents/Dockerfile +++ b/docker/agents/Dockerfile @@ -14,7 +14,8 @@ RUN useradd -m -u 1000 -s /bin/bash agent COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh -USER agent +# Entrypoint runs as root to start the cron daemon; +# cron jobs execute as the agent user (crontab -u agent). WORKDIR /home/agent ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/agents/entrypoint.sh b/docker/agents/entrypoint.sh index 00d5708..ce36e89 100644 --- a/docker/agents/entrypoint.sh +++ b/docker/agents/entrypoint.sh @@ -1,20 +1,22 @@ #!/usr/bin/env bash set -euo pipefail -# entrypoint.sh — Start agent container with cron and stay alive +# entrypoint.sh — Start agent container with cron in foreground # -# Installs crontab entries from project TOMLs found in the factory -# mount, then runs cron in the background and tails the log. +# Runs as root inside the container. Installs crontab entries for the +# agent user from project TOMLs, then starts cron in the foreground. +# All cron jobs execute as the agent user (UID 1000). -DISINTO_DIR="${HOME}/disinto" -LOGFILE="${HOME}/data/agent-entrypoint.log" -mkdir -p "${HOME}/data" +DISINTO_DIR="/home/agent/disinto" +LOGFILE="/home/agent/data/agent-entrypoint.log" +mkdir -p /home/agent/data +chown agent:agent /home/agent/data log() { printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" | tee -a "$LOGFILE" } -# Build crontab from project TOMLs +# Build crontab from project TOMLs and install for the agent user. install_project_crons() { local cron_lines="" for toml in "${DISINTO_DIR}"/projects/*.toml; do @@ -34,8 +36,8 @@ with open(sys.argv[1], 'rb') as f: done if [ -n "$cron_lines" ]; then - printf '%s\n' "$cron_lines" | crontab - - log "Installed crontab for projects" + printf '%s\n' "$cron_lines" | crontab -u agent - + log "Installed crontab for agent user" else log "No project TOMLs found — crontab empty" fi @@ -44,8 +46,6 @@ with open(sys.argv[1], 'rb') as f: log "Agent container starting" install_project_crons -# Keep container alive — cron runs in foreground via tail on log -# (cron daemon needs root; since we run as agent, we use a polling approach -# or the host cron can be used via docker compose exec) -log "Agent container ready — waiting for work" -exec tail -f /dev/null +# Run cron in the foreground. Cron jobs execute as the agent user. +log "Starting cron daemon" +exec cron -f From 4f99a7a26a979984149e2cfd84ba0c444d0c00c3 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 24 Mar 2026 20:36:55 +0000 Subject: [PATCH 3/3] fix: Clean up decrypted secrets on failure, verify Claude CLI install (#618) Add EXIT trap in disinto_up() so the plaintext .env is removed even if docker compose up fails. Previously set -euo pipefail would abort before the cleanup block, leaving secrets on disk. Replace the silent || true in the Dockerfile with an explicit claude --version check so the build fails visibly if the CLI cannot be installed. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 3 ++- docker/agents/Dockerfile | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/disinto b/bin/disinto index bbe6d43..5487d75 100755 --- a/bin/disinto +++ b/bin/disinto @@ -1226,13 +1226,14 @@ disinto_up() { if [ -f "$enc_file" ] && command -v sops &>/dev/null && [ ! -f "$env_file" ]; then tmp_env="${env_file}" sops -d --output-type dotenv "$enc_file" > "$tmp_env" + trap '[ -n "${tmp_env:-}" ] && rm -f "$tmp_env"' EXIT echo "Decrypted secrets for compose" fi docker compose -f "$compose_file" up -d "$@" echo "Stack is up" - # Clean up temp .env + # Clean up temp .env (also handled by EXIT trap if compose fails) if [ -n "$tmp_env" ] && [ -f "$tmp_env" ]; then rm -f "$tmp_env" echo "Removed temporary .env" diff --git a/docker/agents/Dockerfile b/docker/agents/Dockerfile index 2dc8a22..18cc7f2 100644 --- a/docker/agents/Dockerfile +++ b/docker/agents/Dockerfile @@ -4,9 +4,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ bash curl git jq tmux cron python3 openssh-client ca-certificates \ && rm -rf /var/lib/apt/lists/* -# Claude CLI +# Claude CLI — install and verify RUN curl -fsSL https://cli.anthropic.com/install.sh | sh \ - && mv /root/.claude/local/claude /usr/local/bin/claude || true + && cp "$(find /root -name claude -type f 2>/dev/null | head -1)" /usr/local/bin/claude \ + && claude --version # Non-root user RUN useradd -m -u 1000 -s /bin/bash agent