From 53a1fe397b204b6617a708d906fc744449a22232 Mon Sep 17 00:00:00 2001 From: Agent Date: Thu, 16 Apr 2026 12:00:58 +0000 Subject: [PATCH] fix: hire-an-agent does not persist per-agent secrets to .env (#847) --- bin/disinto | 118 ++++++++++++++++++++++++++++++++++++++++++- docs/agents-llama.md | 45 +++++++++++++++++ lib/hire-agent.sh | 38 ++++++++++++++ 3 files changed, 200 insertions(+), 1 deletion(-) diff --git a/bin/disinto b/bin/disinto index 4f06b5e..69e34dd 100755 --- a/bin/disinto +++ b/bin/disinto @@ -60,7 +60,7 @@ Usage: Read CI logs from Woodpecker SQLite disinto release Create vault PR for release (e.g., v1.2.0) disinto hire-an-agent [--formula ] [--local-model ] [--model ] - Hire a new agent (create user + .profile repo) + Hire a new agent (create user + .profile repo; re-run to rotate credentials) disinto agent Manage agent state (enable/disable) disinto edge [options] Manage edge tunnel registrations @@ -1757,6 +1757,119 @@ _regen_file() { fi } +# Validate that required environment variables are present for all services +# that reference them in docker-compose.yml +_validate_env_vars() { + local env_file="${FACTORY_ROOT}/.env" + local errors=0 + local -a missing_vars=() + + # Load env vars from .env file into associative array + declare -A env_vars + if [ -f "$env_file" ]; then + while IFS='=' read -r key value; do + # Skip empty lines and comments + [[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue + env_vars["$key"]="$value" + done < "$env_file" + fi + + # Check for local-model agent services + # Each [agents.*] section in projects/*.toml requires: + # - FORGE_TOKEN_ + # - FORGE_PASS_ + # - ANTHROPIC_BASE_URL (local model) OR ANTHROPIC_API_KEY (Anthropic backend) + + # Parse projects/*.toml for [agents.*] sections + local projects_dir="${FACTORY_ROOT}/projects" + for toml in "${projects_dir}"/*.toml; do + [ -f "$toml" ] || continue + + # Extract agent config using Python + while IFS='|' read -r service_name forge_user base_url _api_key; do + [ -n "$service_name" ] || continue + [ -n "$forge_user" ] || continue + [ -n "$base_url" ] || continue + + # Derive variable names (user -> USER_UPPER) + local user_upper + user_upper=$(echo "$forge_user" | tr 'a-z-' 'A-Z_') + local token_var="FORGE_TOKEN_${user_upper}" + local pass_var="FORGE_PASS_${user_upper}" + + # Check token + if [ -z "${env_vars[$token_var]:-}" ]; then + missing_vars+=("$token_var (for agent ${service_name}/${forge_user})") + errors=$((errors + 1)) + fi + + # Check password + if [ -z "${env_vars[$pass_var]:-}" ]; then + missing_vars+=("$pass_var (for agent ${service_name}/${forge_user})") + errors=$((errors + 1)) + fi + + # Check backend URL or API key + if [ -n "$base_url" ]; then + # Local model: needs ANTHROPIC_BASE_URL + if [ -z "${env_vars[ANTHROPIC_BASE_URL]:-}" ]; then + missing_vars+=("ANTHROPIC_BASE_URL (for agent ${service_name})") + errors=$((errors + 1)) + fi + else + # Anthropic backend: needs ANTHROPIC_API_KEY + if [ -z "${env_vars[ANTHROPIC_API_KEY]:-}" ]; then + missing_vars+=("ANTHROPIC_API_KEY (for agent ${service_name})") + errors=$((errors + 1)) + fi + fi + + done < <(python3 -c ' +import sys, tomllib, re + +with open(sys.argv[1], "rb") as f: + cfg = tomllib.load(f) + +agents = cfg.get("agents", {}) +for name, config in agents.items(): + if not isinstance(config, dict): + continue + + base_url = config.get("base_url", "") + model = config.get("model", "") + api_key = config.get("api_key", "") + forge_user = config.get("forge_user", f"{name}-bot") + + safe_name = name.lower() + safe_name = re.sub(r"[^a-z0-9]", "-", safe_name) + + print(f"{safe_name}|{forge_user}|{base_url}|{api_key}") +' "$toml" 2>/dev/null) + done + + # Check for legacy ENABLE_LLAMA_AGENT services + if [ "${env_vars[ENABLE_LLAMA_AGENT]:-0}" = "1" ]; then + if [ -z "${env_vars[FORGE_TOKEN_LLAMA]:-}" ]; then + missing_vars+=("FORGE_TOKEN_LLAMA (ENABLE_LLAMA_AGENT=1)") + errors=$((errors + 1)) + fi + if [ -z "${env_vars[FORGE_PASS_LLAMA]:-}" ]; then + missing_vars+=("FORGE_PASS_LLAMA (ENABLE_LLAMA_AGENT=1)") + errors=$((errors + 1)) + fi + fi + + if [ "$errors" -gt 0 ]; then + echo "Error: missing required environment variables:" >&2 + for var in "${missing_vars[@]}"; do + echo " - $var" >&2 + done + echo "" >&2 + echo "Run 'disinto hire-an-agent ' to create the agent and write credentials to .env" >&2 + exit 1 + fi +} + disinto_up() { local compose_file="${FACTORY_ROOT}/docker-compose.yml" local caddyfile="${FACTORY_ROOT}/docker/Caddyfile" @@ -1766,6 +1879,9 @@ disinto_up() { exit 1 fi + # Validate environment variables before proceeding + _validate_env_vars + # Parse --no-regen flag; remaining args pass through to docker compose local no_regen=false local -a compose_args=() diff --git a/docs/agents-llama.md b/docs/agents-llama.md index 88622a7..317876d 100644 --- a/docs/agents-llama.md +++ b/docs/agents-llama.md @@ -26,6 +26,51 @@ ANTHROPIC_BASE_URL=http://host.docker.internal:8081 # llama-server endpoint Then regenerate the compose file (`disinto init ...`) and bring the stack up. +## Hiring a new agent + +Use `disinto hire-an-agent` to create a Forgejo user, API token, and password, +and write all required credentials to `.env`: + +```bash +# Local model agent +disinto hire-an-agent dev-qwen dev \ + --local-model http://10.10.10.1:8081 \ + --model unsloth/Qwen3.5-35B-A3B + +# Anthropic backend agent (requires ANTHROPIC_API_KEY in environment) +disinto hire-an-agent dev-qwen dev +``` + +The command writes the following to `.env`: +- `FORGE_TOKEN_` — derived from the agent's Forgejo username (e.g., `FORGE_TOKEN_DEV_QWEN`) +- `FORGE_PASS_` — the agent's Forgejo password +- `ANTHROPIC_BASE_URL` (local model) or `ANTHROPIC_API_KEY` (Anthropic backend) + +## Rotation + +Re-running `disinto hire-an-agent ` rotates credentials idempotently: + +```bash +# Re-hire the same agent to rotate token and password +disinto hire-an-agent dev-qwen dev \ + --local-model http://10.10.10.1:8081 \ + --model unsloth/Qwen3.5-35B-A3B + +# The command will: +# 1. Detect the user already exists +# 2. Reset the password to a new random value +# 3. Create a new API token +# 4. Update .env with the new credentials +``` + +This is the recommended way to rotate agent credentials. The `.env` file is +updated in place, so no manual editing is required. + +If you need to manually rotate credentials, you can: +1. Generate a new token in Forgejo admin UI +2. Edit `.env` and replace `FORGE_TOKEN_` and `FORGE_PASS_` +3. Restart the agent service: `docker compose restart disinto-agents-` + ### Running all 7 roles (agents-llama-all) ```bash diff --git a/lib/hire-agent.sh b/lib/hire-agent.sh index 1140f73..5ebe5a1 100644 --- a/lib/hire-agent.sh +++ b/lib/hire-agent.sh @@ -252,6 +252,44 @@ disinto_hire_an_agent() { export "${pass_var}=${user_pass}" fi + # Step 1.7: Write backend credentials to .env (#847). + # Local-model agents need ANTHROPIC_BASE_URL; Anthropic-backend agents need ANTHROPIC_API_KEY. + # These must be persisted so the container can start with valid credentials. + echo "" + echo "Step 1.7: Writing backend credentials to .env..." + + if [ -n "$local_model" ]; then + # Local model agent: write ANTHROPIC_BASE_URL + local backend_var="ANTHROPIC_BASE_URL" + local backend_val="$local_model" + if grep -q "^${backend_var}=" "$env_file" 2>/dev/null; then + sed -i "s|^${backend_var}=.*|${backend_var}=${backend_val}|" "$env_file" + echo " ${backend_var} updated" + else + printf '%s=%s\n' "$backend_var" "$backend_val" >> "$env_file" + echo " ${backend_var} saved" + fi + export "${backend_var}=${backend_val}" + else + # Anthropic backend: check if ANTHROPIC_API_KEY is set, write it if present + if [ -n "${ANTHROPIC_API_KEY:-}" ]; then + local backend_var="ANTHROPIC_API_KEY" + local backend_val="$ANTHROPIC_API_KEY" + local escaped_key + escaped_key=$(printf '%s\n' "$backend_val" | sed 's/[&/\]/\\&/g') + if grep -q "^${backend_var}=" "$env_file" 2>/dev/null; then + sed -i "s|^${backend_var}=.*|${backend_var}=${escaped_key}|" "$env_file" + echo " ${backend_var} updated" + else + printf '%s=%s\n' "$backend_var" "$backend_val" >> "$env_file" + echo " ${backend_var} saved" + fi + export "${backend_var}=${backend_val}" + else + echo " Note: ANTHROPIC_API_KEY not set — required for Anthropic backend agents" + fi + fi + # Step 1.6: Add the new agent as a write collaborator on the project repo (#856). # Without this, PATCH /issues/{n} {assignees:[agent]} returns 403 Forbidden and # the dev-agent polls forever logging "claim lost to — skipping" (see