Merge pull request 'fix: fix: disinto hire-an-agent + compose generator defects blocking multi-llama-dev parallel operation (#834)' (#838) from fix/issue-834 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

This commit is contained in:
dev-bot 2026-04-16 09:12:04 +00:00
commit 0850e83ec6
3 changed files with 63 additions and 16 deletions

View file

@ -25,8 +25,16 @@ FORGE_URL=http://localhost:3000 # [CONFIG] local Forgejo instance
# - FORGE_TOKEN_<BOT> = API token for REST calls (user identity via /api/v1/user) # - FORGE_TOKEN_<BOT> = API token for REST calls (user identity via /api/v1/user)
# - FORGE_PASS_<BOT> = password for git HTTP push (#361, Forgejo 11.x limitation) # - FORGE_PASS_<BOT> = password for git HTTP push (#361, Forgejo 11.x limitation)
# #
# Local-model agents (agents-llama) use FORGE_TOKEN_LLAMA / FORGE_PASS_LLAMA # Local-model agents hired with `disinto hire-an-agent` are keyed by *agent
# with FORGE_BOT_USER_LLAMA=dev-qwen to ensure correct attribution (#563). # 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_TOKEN= # [SECRET] dev-bot API token (default for all agents)
FORGE_PASS= # [SECRET] dev-bot password for git HTTP push (#361) FORGE_PASS= # [SECRET] dev-bot password for git HTTP push (#361)
FORGE_TOKEN_LLAMA= # [SECRET] dev-qwen API token (for agents-llama) FORGE_TOKEN_LLAMA= # [SECRET] dev-qwen API token (for agents-llama)

View file

@ -97,6 +97,13 @@ _generate_local_model_services() {
POLL_INTERVAL) poll_interval_val="$value" ;; POLL_INTERVAL) poll_interval_val="$value" ;;
---) ---)
if [ -n "$service_name" ] && [ -n "$base_url" ]; then 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" <<EOF cat >> "$temp_file" <<EOF
agents-${service_name}: agents-${service_name}:
@ -107,7 +114,7 @@ _generate_local_model_services() {
- apparmor=unconfined - apparmor=unconfined
volumes: volumes:
- agents-${service_name}-data:/home/agent/data - agents-${service_name}-data:/home/agent/data
- project-repos:/home/agent/repos - project-repos-${service_name}:/home/agent/repos
- \${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:\${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared} - \${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:\${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
- \${CLAUDE_CONFIG_FILE:-\${HOME}/.claude.json}:/home/agent/.claude.json:ro - \${CLAUDE_CONFIG_FILE:-\${HOME}/.claude.json}:/home/agent/.claude.json:ro
- \${CLAUDE_BIN_DIR}:/usr/local/bin/claude:ro - \${CLAUDE_BIN_DIR}:/usr/local/bin/claude:ro
@ -115,9 +122,9 @@ _generate_local_model_services() {
environment: environment:
FORGE_URL: http://forgejo:3000 FORGE_URL: http://forgejo:3000
FORGE_REPO: ${FORGE_REPO:-disinto-admin/disinto} FORGE_REPO: ${FORGE_REPO:-disinto-admin/disinto}
# Use llama-specific credentials if available, otherwise fall back to main FORGE_TOKEN # Per-agent credentials keyed by forge_user (#834 Gap 3).
FORGE_TOKEN: \${FORGE_TOKEN_LLAMA:-\${FORGE_TOKEN:-}} FORGE_TOKEN: \${FORGE_TOKEN_${user_upper}:-}
FORGE_PASS: \${FORGE_PASS_LLAMA:-\${FORGE_PASS:-}} FORGE_PASS: \${FORGE_PASS_${user_upper}:-}
FORGE_REVIEW_TOKEN: \${FORGE_REVIEW_TOKEN:-} FORGE_REVIEW_TOKEN: \${FORGE_REVIEW_TOKEN:-}
FORGE_BOT_USERNAMES: \${FORGE_BOT_USERNAMES:-} FORGE_BOT_USERNAMES: \${FORGE_BOT_USERNAMES:-}
AGENT_ROLES: "${roles}" AGENT_ROLES: "${roles}"
@ -153,13 +160,18 @@ _generate_local_model_services() {
EOF EOF
has_services=true has_services=true
fi fi
# Collect volume name for later # Collect per-agent volume names for later (#834 Gap 4: project-repos
local vol_name=" agents-${service_name}-data:" # must be per-agent so concurrent llama devs don't race on
# /home/agent/repos/_factory or state/.dev-active).
local vol_data=" agents-${service_name}-data:"
local vol_repos=" project-repos-${service_name}:"
if [ -n "$all_vols" ]; then if [ -n "$all_vols" ]; then
all_vols="${all_vols} all_vols="${all_vols}
${vol_name}" ${vol_data}
${vol_repos}"
else else
all_vols="${vol_name}" all_vols="${vol_data}
${vol_repos}"
fi fi
service_name="" base_url="" model="" roles="" api_key="" forge_user="" compact_pct="" poll_interval_val="" service_name="" base_url="" model="" roles="" api_key="" forge_user="" compact_pct="" poll_interval_val=""
;; ;;
@ -216,8 +228,14 @@ for name, config in agents.items():
# Add local-model volumes to the volumes section # Add local-model volumes to the volumes section
if [ -n "$all_vols" ]; then if [ -n "$all_vols" ]; then
# Escape embedded newlines as literal \n so sed's s/// replacement
# tolerates multi-line $all_vols (needed once >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 # 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 fi
mv "$temp_compose" "$compose_file" mv "$temp_compose" "$compose_file"

View file

@ -167,10 +167,14 @@ disinto_hire_an_agent() {
echo "" echo ""
echo "Step 1.5: Generating Forge token for '${agent_name}'..." echo "Step 1.5: Generating Forge token for '${agent_name}'..."
# Convert role to uppercase token variable name (e.g., architect -> FORGE_ARCHITECT_TOKEN) # Key per-agent credentials by *agent name*, not role (#834 Gap 1).
local role_upper # Two agents with the same role (e.g. two `dev` agents) must not collide on
role_upper=$(echo "$role" | tr '[:lower:]' '[:upper:]') # FORGE_<ROLE>_TOKEN — the compose generator looks up FORGE_TOKEN_<USER_UPPER>
local token_var="FORGE_${role_upper}_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) # Generate token using the user's password (basic auth)
local agent_token="" local agent_token=""
@ -194,7 +198,7 @@ disinto_hire_an_agent() {
if [ -z "$agent_token" ]; then if [ -z "$agent_token" ]; then
echo " Warning: failed to create API token for '${agent_name}'" >&2 echo " Warning: failed to create API token for '${agent_name}'" >&2
else 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 if grep -q "^${token_var}=" "$env_file" 2>/dev/null; then
# Use sed with alternative delimiter and proper escaping for special chars in token # Use sed with alternative delimiter and proper escaping for special chars in token
local escaped_token local escaped_token
@ -208,6 +212,23 @@ disinto_hire_an_agent() {
export "${token_var}=${agent_token}" export "${token_var}=${agent_token}"
fi fi
# Persist FORGE_PASS_<AGENT_UPPER> 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 # Step 2: Create .profile repo on Forgejo
echo "" echo ""
echo "Step 2: Creating '${agent_name}/.profile' repo (if not exists)..." echo "Step 2: Creating '${agent_name}/.profile' repo (if not exists)..."