From 43dc86d84cddaf850cc11a7d581adc9e30bb071e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 08:55:45 +0000 Subject: [PATCH] fix: fix: disinto hire-an-agent + compose generator defects blocking multi-llama-dev parallel operation (#834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hiring a second llama-backed dev agent (e.g. `dev-qwen2`) alongside `dev-qwen` tripped four defects that prevented safe parallel operation. Gap 1 — hire-agent keyed per-agent token as FORGE__TOKEN, so two dev-role agents overwrote each other's token in .env. Re-key by agent name via `tr 'a-z-' 'A-Z_'`: FORGE_TOKEN_. Gap 2 — hire-agent generated a random FORGE_PASS but never wrote it to .env. The container's git credential helper needs both token and pass to push over HTTPS (#361). Persist FORGE_PASS_ with the same update-in-place idempotency as the token. Gap 3 — _generate_local_model_services hardcoded FORGE_TOKEN_LLAMA for every local-model service, forcing all hired llama agents to share one Forgejo identity. Derive USER_UPPER from the TOML's `forge_user` field and emit \${FORGE_TOKEN_:-} per service. Gap 4 — every local-model service mounted the shared `project-repos` volume, so concurrent llama devs collided on /_factory worktree and state/.dev-active. Switch to per-agent `project-repos-` and emit the matching top-level volume. Also escape embedded newlines in `$all_vols` before the sed insertion so multi-agent volume lists don't unterminate the substitute command. .env.example documents the new FORGE_TOKEN_ / FORGE_PASS_ naming convention (and preserves the legacy FORGE_TOKEN_LLAMA path used by the ENABLE_LLAMA_AGENT=1 singleton build). Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 12 ++++++++++-- lib/generators.sh | 36 +++++++++++++++++++++++++++--------- lib/hire-agent.sh | 31 ++++++++++++++++++++++++++----- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index 7e76ec2..c1c0b98 100644 --- a/.env.example +++ b/.env.example @@ -25,8 +25,16 @@ FORGE_URL=http://localhost:3000 # [CONFIG] local Forgejo instance # - FORGE_TOKEN_ = API token for REST calls (user identity via /api/v1/user) # - FORGE_PASS_ = password for git HTTP push (#361, Forgejo 11.x limitation) # -# Local-model agents (agents-llama) use FORGE_TOKEN_LLAMA / FORGE_PASS_LLAMA -# with FORGE_BOT_USER_LLAMA=dev-qwen to ensure correct attribution (#563). +# Local-model agents hired with `disinto hire-an-agent` are keyed by *agent +# name* (not role), so multiple local-model dev agents can coexist without +# colliding on credentials (#834). For an agent named `dev-qwen2` the vars are: +# - FORGE_TOKEN_DEV_QWEN2 +# - FORGE_PASS_DEV_QWEN2 +# Name conversion: tr 'a-z-' 'A-Z_' (lowercase→UPPER, hyphens→underscores). +# The compose generator looks these up via the agent's `forge_user` field in +# the project TOML. The pre-existing `dev-qwen` llama agent uses +# FORGE_TOKEN_LLAMA / FORGE_PASS_LLAMA (kept for backwards-compat with the +# legacy `ENABLE_LLAMA_AGENT=1` single-agent path). FORGE_TOKEN= # [SECRET] dev-bot API token (default for all agents) FORGE_PASS= # [SECRET] dev-bot password for git HTTP push (#361) FORGE_TOKEN_LLAMA= # [SECRET] dev-qwen API token (for agents-llama) diff --git a/lib/generators.sh b/lib/generators.sh index 02af667..af08aa2 100644 --- a/lib/generators.sh +++ b/lib/generators.sh @@ -97,6 +97,13 @@ _generate_local_model_services() { POLL_INTERVAL) poll_interval_val="$value" ;; ---) if [ -n "$service_name" ] && [ -n "$base_url" ]; then + # Per-agent FORGE_TOKEN / FORGE_PASS lookup (#834 Gap 3). + # Two hired llama agents must not share the same Forgejo identity, + # so we key the env-var lookup by forge_user (which hire-agent.sh + # writes as the Forgejo username). Apply the same tr 'a-z-' 'A-Z_' + # convention as hire-agent.sh Gap 1 so the names match. + local user_upper + user_upper=$(echo "$forge_user" | tr 'a-z-' 'A-Z_') cat >> "$temp_file" <1 local-model agent is + # configured — without this, the second agent's volume entry would + # unterminate the sed expression). + local all_vols_escaped + all_vols_escaped=$(printf '%s' "$all_vols" | sed ':a;N;$!ba;s/\n/\\n/g') # Find the volumes section and add the new volumes - sed -i "/^volumes:/{n;:a;n;/^[a-z]/!{s/$/\n$all_vols/;b};ba}" "$temp_compose" + sed -i "/^volumes:/{n;:a;n;/^[a-z]/!{s/$/\n$all_vols_escaped/;b};ba}" "$temp_compose" fi mv "$temp_compose" "$compose_file" diff --git a/lib/hire-agent.sh b/lib/hire-agent.sh index 91d1fc8..49ab8ae 100644 --- a/lib/hire-agent.sh +++ b/lib/hire-agent.sh @@ -167,10 +167,14 @@ disinto_hire_an_agent() { echo "" echo "Step 1.5: Generating Forge token for '${agent_name}'..." - # Convert role to uppercase token variable name (e.g., architect -> FORGE_ARCHITECT_TOKEN) - local role_upper - role_upper=$(echo "$role" | tr '[:lower:]' '[:upper:]') - local token_var="FORGE_${role_upper}_TOKEN" + # Key per-agent credentials by *agent name*, not role (#834 Gap 1). + # Two agents with the same role (e.g. two `dev` agents) must not collide on + # FORGE__TOKEN — the compose generator looks up FORGE_TOKEN_ + # where USER_UPPER = tr 'a-z-' 'A-Z_' of the agent's forge_user. + local agent_upper + agent_upper=$(echo "$agent_name" | tr 'a-z-' 'A-Z_') + local token_var="FORGE_TOKEN_${agent_upper}" + local pass_var="FORGE_PASS_${agent_upper}" # Generate token using the user's password (basic auth) local agent_token="" @@ -194,7 +198,7 @@ disinto_hire_an_agent() { if [ -z "$agent_token" ]; then echo " Warning: failed to create API token for '${agent_name}'" >&2 else - # Store token in .env under the role-specific variable name + # Store token in .env under the per-agent variable name if grep -q "^${token_var}=" "$env_file" 2>/dev/null; then # Use sed with alternative delimiter and proper escaping for special chars in token local escaped_token @@ -208,6 +212,23 @@ disinto_hire_an_agent() { export "${token_var}=${agent_token}" fi + # Persist FORGE_PASS_ to .env (#834 Gap 2). + # The container's git credential helper (docker/agents/entrypoint.sh) needs + # both FORGE_TOKEN_* and FORGE_PASS_* to pass HTTPS auth for git push + # (Forgejo 11.x rejects API tokens for git push, #361). + if [ -n "${user_pass:-}" ]; then + local escaped_pass + escaped_pass=$(printf '%s\n' "$user_pass" | sed 's/[&/\]/\\&/g') + if grep -q "^${pass_var}=" "$env_file" 2>/dev/null; then + sed -i "s|^${pass_var}=.*|${pass_var}=${escaped_pass}|" "$env_file" + echo " ${agent_name} password updated (${pass_var})" + else + printf '%s=%s\n' "$pass_var" "$user_pass" >> "$env_file" + echo " ${agent_name} password saved (${pass_var})" + fi + export "${pass_var}=${user_pass}" + fi + # Step 2: Create .profile repo on Forgejo echo "" echo "Step 2: Creating '${agent_name}/.profile' repo (if not exists)..." -- 2.49.1