From 721d7a6077c96b1ea96624d75692d6439e094b63 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 11:55:56 +0000 Subject: [PATCH 1/4] fix: bug: TOML [agents.X] section name with dash crashes load-project.sh (#862) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TOML allows dashes in bare keys, so `[agents.dev-qwen2]` is a valid section. Before this fix, load-project.sh derived bash var names via Python `.upper()` alone, which kept the dash and produced `AGENT_DEV-QWEN2_BASE_URL` — an invalid shell identifier. Under `set -euo pipefail` the subsequent `export` aborted the whole file, silently taking the factory down on the N+1 run after a dashed agent was hired via `disinto hire-an-agent`. Normalize via `.upper().replace('-', '_')` to match the `tr 'a-z-' 'A-Z_'` convention already used by hire-agent.sh (#834) and generators.sh (#852). Also harden hire-agent.sh to reject invalid agent names at hire time (before any Forgejo side effects), so unparseable TOML sections never land on disk. - `lib/load-project.sh` — dash-to-underscore in emitted shell var names - `lib/hire-agent.sh` — validate agent name against `^[a-z]([a-z0-9]|-[a-z0-9])*$` up front - `tests/lib-load-project.bats` — regression guard covering the parse path and the hire-time reject path Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/hire-agent.sh | 23 +++++ lib/load-project.sh | 18 ++-- tests/lib-load-project.bats | 186 ++++++++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 tests/lib-load-project.bats diff --git a/lib/hire-agent.sh b/lib/hire-agent.sh index 994103a..1140f73 100644 --- a/lib/hire-agent.sh +++ b/lib/hire-agent.sh @@ -30,6 +30,29 @@ disinto_hire_an_agent() { echo "Usage: disinto hire-an-agent [--formula ] [--local-model ] [--model ] [--poll-interval ]" >&2 exit 1 fi + + # Validate agent name before any side effects (Forgejo user creation, TOML + # write, token issuance). The name flows through several systems that have + # stricter rules than the raw TOML spec: + # - load-project.sh emits shell vars keyed by the name (dashes are mapped + # to underscores via tr 'a-z-' 'A-Z_') + # - generators.sh emits a docker-compose service name `agents-` and + # uppercases it for env var keys (#852 tracks the `^^` bug; we keep the + # grammar tight here so that fix can happen without re-validation) + # - Forgejo usernames are lowercase alnum + dash + # Constraint: start with a lowercase letter, contain only [a-z0-9-], end + # with a lowercase letter or digit (no trailing dash), no consecutive + # dashes. Rejecting at hire-time prevents unparseable TOML sections like + # [agents.dev-qwen2] from landing on disk and crashing load-project.sh on + # the next `disinto up` (#862). + if ! [[ "$agent_name" =~ ^[a-z]([a-z0-9]|-[a-z0-9])*$ ]]; then + echo "Error: invalid agent name '${agent_name}'" >&2 + echo " Agent names must match: ^[a-z]([a-z0-9]|-[a-z0-9])*$" >&2 + echo " (lowercase letters/digits/single dashes, starts with letter, ends with alphanumeric)" >&2 + echo " Examples: dev, dev-qwen2, review-qwen, planner" >&2 + exit 1 + fi + shift 2 # Parse flags diff --git a/lib/load-project.sh b/lib/load-project.sh index 0745276..5ad23cc 100755 --- a/lib/load-project.sh +++ b/lib/load-project.sh @@ -129,20 +129,26 @@ agents = cfg.get('agents', {}) for name, config in agents.items(): if not isinstance(config, dict): continue + # Normalize the TOML section key into a valid shell identifier fragment. + # TOML allows dashes in bare keys (e.g. [agents.dev-qwen2]), but POSIX + # shell var names cannot contain '-'. Match the 'tr a-z- A-Z_' convention + # used in hire-agent.sh (#834) and generators.sh (#852) so the var names + # stay consistent across the stack. + safe = name.upper().replace('-', '_') # Emit variables in uppercase with the agent name if 'base_url' in config: - print(f'AGENT_{name.upper()}_BASE_URL={config[\"base_url\"]}') + print(f'AGENT_{safe}_BASE_URL={config[\"base_url\"]}') if 'model' in config: - print(f'AGENT_{name.upper()}_MODEL={config[\"model\"]}') + print(f'AGENT_{safe}_MODEL={config[\"model\"]}') if 'api_key' in config: - print(f'AGENT_{name.upper()}_API_KEY={config[\"api_key\"]}') + print(f'AGENT_{safe}_API_KEY={config[\"api_key\"]}') if 'roles' in config: roles = ' '.join(config['roles']) if isinstance(config['roles'], list) else config['roles'] - print(f'AGENT_{name.upper()}_ROLES={roles}') + print(f'AGENT_{safe}_ROLES={roles}') if 'forge_user' in config: - print(f'AGENT_{name.upper()}_FORGE_USER={config[\"forge_user\"]}') + print(f'AGENT_{safe}_FORGE_USER={config[\"forge_user\"]}') if 'compact_pct' in config: - print(f'AGENT_{name.upper()}_COMPACT_PCT={config[\"compact_pct\"]}') + print(f'AGENT_{safe}_COMPACT_PCT={config[\"compact_pct\"]}') " "$_PROJECT_TOML" 2>/dev/null) || true if [ -n "$_AGENT_VARS" ]; then diff --git a/tests/lib-load-project.bats b/tests/lib-load-project.bats new file mode 100644 index 0000000..89e82be --- /dev/null +++ b/tests/lib-load-project.bats @@ -0,0 +1,186 @@ +#!/usr/bin/env bats +# ============================================================================= +# tests/lib-load-project.bats — Regression guard for the #862 fix. +# +# TOML allows dashes in bare keys, so `[agents.dev-qwen2]` is a valid section +# header. Before #862, load-project.sh translated the section name into a +# shell variable name via Python's `.upper()` alone, which kept the dash and +# produced `AGENT_DEV-QWEN2_BASE_URL`. `export "AGENT_DEV-QWEN2_..."` is +# rejected by bash ("not a valid identifier"), and with `set -euo pipefail` +# anywhere up-stack that error aborts load-project.sh — effectively crashing +# the factory on the N+1 run after a dashed agent was hired. +# +# The fix normalizes via `.upper().replace('-', '_')`, matching the +# `tr 'a-z-' 'A-Z_'` convention already used in hire-agent.sh and +# generators.sh. +# ============================================================================= + +setup() { + ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + TOML="${BATS_TEST_TMPDIR}/test.toml" +} + +@test "dashed [agents.*] section name parses without error" { + cat > "$TOML" < "$TOML" < "$TOML" < Date: Thu, 16 Apr 2026 12:00:58 +0000 Subject: [PATCH 2/4] 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 994103a..91e7250 100644 --- a/lib/hire-agent.sh +++ b/lib/hire-agent.sh @@ -229,6 +229,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 From 53a1fe397b204b6617a708d906fc744449a22232 Mon Sep 17 00:00:00 2001 From: Agent Date: Thu, 16 Apr 2026 12:00:58 +0000 Subject: [PATCH 3/4] 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 From a3eb33ccf76582fef4ce686c3b216b44220b2d4a Mon Sep 17 00:00:00 2001 From: Agent Date: Thu, 16 Apr 2026 12:28:57 +0000 Subject: [PATCH 4/4] fix: _validate_env_vars skips Anthropic-backend agents + missing sed escaping - bin/disinto: Remove '[ -n "$base_url" ] || continue' guard that caused all Anthropic-backend agents to be silently skipped during validation. The base_url check is now scoped only to backend-credential selection. - lib/hire-agent.sh: Add sed escaping for ANTHROPIC_BASE_URL value before sed substitution (same pattern as ANTHROPIC_API_KEY at line 256). Fixes AI review BLOCKER and MINOR issues on PR #866. --- bin/disinto | 3 +-- lib/hire-agent.sh | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/disinto b/bin/disinto index 69e34dd..dc56f39 100755 --- a/bin/disinto +++ b/bin/disinto @@ -1789,7 +1789,6 @@ _validate_env_vars() { 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 @@ -1809,7 +1808,7 @@ _validate_env_vars() { errors=$((errors + 1)) fi - # Check backend URL or API key + # Check backend URL or API key (conditional based on base_url presence) if [ -n "$base_url" ]; then # Local model: needs ANTHROPIC_BASE_URL if [ -z "${env_vars[ANTHROPIC_BASE_URL]:-}" ]; then diff --git a/lib/hire-agent.sh b/lib/hire-agent.sh index 5ebe5a1..149845b 100644 --- a/lib/hire-agent.sh +++ b/lib/hire-agent.sh @@ -262,8 +262,10 @@ disinto_hire_an_agent() { # Local model agent: write ANTHROPIC_BASE_URL local backend_var="ANTHROPIC_BASE_URL" local backend_val="$local_model" + local escaped_val + escaped_val=$(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}=${backend_val}|" "$env_file" + sed -i "s|^${backend_var}=.*|${backend_var}=${escaped_val}|" "$env_file" echo " ${backend_var} updated" else printf '%s=%s\n' "$backend_var" "$backend_val" >> "$env_file"