From cecfb3374d3a873ef5fae56a55e9d0e6305b096f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 14:18:27 +0000 Subject: [PATCH] fix: bug: bin/disinto init rotates all bot tokens and passwords on every run, invalidating existing cloned repos with embedded credentials (#584) --- bin/disinto | 23 ++++--- lib/forge-setup.sh | 145 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 133 insertions(+), 35 deletions(-) diff --git a/bin/disinto b/bin/disinto index 7f3876a..177de1e 100755 --- a/bin/disinto +++ b/bin/disinto @@ -69,6 +69,7 @@ Init options: --forge-url Forge base URL (default: http://localhost:3000) --bare Skip compose generation (bare-metal setup) --yes Skip confirmation prompts + --rotate-tokens Force regeneration of all bot tokens/passwords (idempotent by default) Hire an agent options: --formula Path to role formula TOML (default: formulas/.toml) @@ -551,15 +552,16 @@ disinto_init() { shift # Parse flags - local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag="" bare=false + local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag="" bare=false rotate_tokens=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 ;; + --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 ;; + --rotate-tokens) rotate_tokens=true; shift ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac done @@ -651,7 +653,12 @@ p.write_text(text) fi # Set up local Forgejo instance (provision if needed, create users/tokens/repo) - setup_forge "$forge_url" "$forge_repo" + if [ "$rotate_tokens" = true ]; then + echo "Note: Forcing token rotation (tokens/passwords will be regenerated)" + setup_forge --rotate-tokens "$forge_url" "$forge_repo" + else + setup_forge "$forge_url" "$forge_repo" + fi # Preflight: verify factory requirements preflight_check "$forge_repo" "$forge_url" diff --git a/lib/forge-setup.sh b/lib/forge-setup.sh index d81e10a..b925103 100644 --- a/lib/forge-setup.sh +++ b/lib/forge-setup.sh @@ -38,10 +38,33 @@ _forgejo_exec() { fi } +# Check if a token already exists in .env (for idempotency) +# Returns 0 if token exists, 1 if it doesn't +_token_exists_in_env() { + local token_var="$1" + local env_file="$2" + grep -q "^${token_var}=" "$env_file" 2>/dev/null +} + +# Check if a password already exists in .env (for idempotency) +# Returns 0 if password exists, 1 if it doesn't +_pass_exists_in_env() { + local pass_var="$1" + local env_file="$2" + grep -q "^${pass_var}=" "$env_file" 2>/dev/null +} + # 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. +# Usage: setup_forge [--rotate-tokens] setup_forge() { + local rotate_tokens=false + # Parse optional --rotate-tokens flag + if [ "$1" = "--rotate-tokens" ]; then + rotate_tokens=true + shift + fi local forge_url="$1" local repo_slug="$2" local use_bare="${DISINTO_BARE:-false}" @@ -319,10 +342,22 @@ setup_forge() { local bot_user bot_pass token token_var pass_var for bot_user in dev-bot review-bot planner-bot gardener-bot vault-bot supervisor-bot predictor-bot architect-bot; do - bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" token_var="${bot_token_vars[$bot_user]}" + pass_var="${bot_pass_vars[$bot_user]}" - # Check if bot user exists + # Check if token already exists in .env + local token_exists=false + if _token_exists_in_env "$token_var" "$env_file"; then + token_exists=true + fi + + # Check if password already exists in .env + local pass_exists=false + if _pass_exists_in_env "$pass_var" "$env_file"; then + pass_exists=true + fi + + # Check if bot user exists on Forgejo local user_exists=false if curl -sf --max-time 5 \ -H "Authorization: token ${admin_token}" \ @@ -330,7 +365,25 @@ setup_forge() { user_exists=true fi + # Skip token/password regeneration if both exist in .env and not forcing rotation + if [ "$token_exists" = true ] && [ "$pass_exists" = true ] && [ "$rotate_tokens" = false ]; then + echo " ${bot_user} token and password preserved (use --rotate-tokens to force)" + # Still export the existing token for use within this run + local existing_token existing_pass + existing_token=$(grep "^${token_var}=" "$env_file" | head -1 | cut -d= -f2-) + existing_pass=$(grep "^${pass_var}=" "$env_file" | head -1 | cut -d= -f2-) + export "${token_var}=${existing_token}" + export "${pass_var}=${existing_pass}" + continue + fi + + # Generate new credentials if: + # - Token doesn't exist (first run) + # - Password doesn't exist (first run) + # - --rotate-tokens flag is set (explicit rotation) if [ "$user_exists" = false ]; then + # User doesn't exist - create it + bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" echo "Creating bot user: ${bot_user}" local create_output if ! create_output=$(_forgejo_exec forgejo admin user create \ @@ -358,16 +411,22 @@ setup_forge() { fi echo " ${bot_user} user created" else - echo " ${bot_user} user exists (resetting password for token generation)" - # User exists but may not have a known password. - # Use admin API to reset the password so we can generate a new token. - _forgejo_exec forgejo admin user change-password \ - --username "${bot_user}" \ - --password "${bot_pass}" \ - --must-change-password=false || { - echo "Error: failed to reset password for existing bot user '${bot_user}'" >&2 - exit 1 - } + # User exists - reset password if needed + echo " ${bot_user} user exists" + if [ "$rotate_tokens" = true ] || [ "$pass_exists" = false ]; then + bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" + _forgejo_exec forgejo admin user change-password \ + --username "${bot_user}" \ + --password "${bot_pass}" \ + --must-change-password=false || { + echo "Error: failed to reset password for existing bot user '${bot_user}'" >&2 + exit 1 + } + echo " ${bot_user} password reset for token generation" + else + # Password exists, get it from .env + bot_pass=$(grep "^${pass_var}=" "$env_file" | head -1 | cut -d= -f2-) + fi fi # Generate token via API (basic auth as the bot user — Forgejo requires @@ -412,7 +471,6 @@ setup_forge() { # Store password in .env for git HTTP push (#361) # Forgejo 11.x API tokens don't work for git push; password auth does. - pass_var="${bot_pass_vars[$bot_user]}" if grep -q "^${pass_var}=" "$env_file" 2>/dev/null; then sed -i "s|^${pass_var}=.*|${pass_var}=${bot_pass}|" "$env_file" else @@ -439,7 +497,19 @@ setup_forge() { llama_token_var="${llama_token_vars[$llama_user]}" llama_pass_var="${llama_pass_vars[$llama_user]}" - # Check if llama bot user exists + # Check if token already exists in .env + local token_exists=false + if _token_exists_in_env "$llama_token_var" "$env_file"; then + token_exists=true + fi + + # Check if password already exists in .env + local pass_exists=false + if _pass_exists_in_env "$llama_pass_var" "$env_file"; then + pass_exists=true + fi + + # Check if llama bot user exists on Forgejo local llama_user_exists=false if curl -sf --max-time 5 \ -H "Authorization: token ${admin_token}" \ @@ -447,10 +517,26 @@ setup_forge() { llama_user_exists=true fi + # Skip token/password regeneration if both exist in .env and not forcing rotation + if [ "$token_exists" = true ] && [ "$pass_exists" = true ] && [ "$rotate_tokens" = false ]; then + echo " ${llama_user} token and password preserved (use --rotate-tokens to force)" + # Still export the existing token for use within this run + local existing_token existing_pass + existing_token=$(grep "^${llama_token_var}=" "$env_file" | head -1 | cut -d= -f2-) + existing_pass=$(grep "^${llama_pass_var}=" "$env_file" | head -1 | cut -d= -f2-) + export "${llama_token_var}=${existing_token}" + export "${llama_pass_var}=${existing_pass}" + continue + fi + + # Generate new credentials if: + # - Token doesn't exist (first run) + # - Password doesn't exist (first run) + # - --rotate-tokens flag is set (explicit rotation) if [ "$llama_user_exists" = false ]; then - echo "Creating llama bot user: ${llama_user}" - # Generate a unique password for this user + # User doesn't exist - create it llama_pass="llama-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" + echo "Creating llama bot user: ${llama_user}" local create_output if ! create_output=$(_forgejo_exec forgejo admin user create \ --username "${llama_user}" \ @@ -477,17 +563,22 @@ setup_forge() { fi echo " ${llama_user} user created" else - echo " ${llama_user} user exists (resetting password for token generation)" - # User exists but may not have a known password. - # Use admin API to reset the password so we can generate a new token. - llama_pass="llama-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" - _forgejo_exec forgejo admin user change-password \ - --username "${llama_user}" \ - --password "${llama_pass}" \ - --must-change-password=false || { - echo "Error: failed to reset password for existing llama bot user '${llama_user}'" >&2 - exit 1 - } + # User exists - reset password if needed + echo " ${llama_user} user exists" + if [ "$rotate_tokens" = true ] || [ "$pass_exists" = false ]; then + llama_pass="llama-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" + _forgejo_exec forgejo admin user change-password \ + --username "${llama_user}" \ + --password "${llama_pass}" \ + --must-change-password=false || { + echo "Error: failed to reset password for existing llama bot user '${llama_user}'" >&2 + exit 1 + } + echo " ${llama_user} password reset for token generation" + else + # Password exists, get it from .env + llama_pass=$(grep "^${llama_pass_var}=" "$env_file" | head -1 | cut -d= -f2-) + fi fi # Generate token via API (basic auth as the llama user)