fix: hire-an-agent does not persist per-agent secrets to .env (#847)
All checks were successful
All checks were successful
This commit is contained in:
parent
c63ca86a3c
commit
37ee38f59a
3 changed files with 200 additions and 1 deletions
118
bin/disinto
118
bin/disinto
|
|
@ -60,7 +60,7 @@ Usage:
|
||||||
Read CI logs from Woodpecker SQLite
|
Read CI logs from Woodpecker SQLite
|
||||||
disinto release <version> Create vault PR for release (e.g., v1.2.0)
|
disinto release <version> Create vault PR for release (e.g., v1.2.0)
|
||||||
disinto hire-an-agent <agent-name> <role> [--formula <path>] [--local-model <url>] [--model <name>]
|
disinto hire-an-agent <agent-name> <role> [--formula <path>] [--local-model <url>] [--model <name>]
|
||||||
Hire a new agent (create user + .profile repo)
|
Hire a new agent (create user + .profile repo; re-run to rotate credentials)
|
||||||
disinto agent <subcommand> Manage agent state (enable/disable)
|
disinto agent <subcommand> Manage agent state (enable/disable)
|
||||||
disinto edge <verb> [options] Manage edge tunnel registrations
|
disinto edge <verb> [options] Manage edge tunnel registrations
|
||||||
|
|
||||||
|
|
@ -1757,6 +1757,119 @@ _regen_file() {
|
||||||
fi
|
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_<USER_UPPER>
|
||||||
|
# - FORGE_PASS_<USER_UPPER>
|
||||||
|
# - 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 <name> <role>' to create the agent and write credentials to .env" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
disinto_up() {
|
disinto_up() {
|
||||||
local compose_file="${FACTORY_ROOT}/docker-compose.yml"
|
local compose_file="${FACTORY_ROOT}/docker-compose.yml"
|
||||||
local caddyfile="${FACTORY_ROOT}/docker/Caddyfile"
|
local caddyfile="${FACTORY_ROOT}/docker/Caddyfile"
|
||||||
|
|
@ -1766,6 +1879,9 @@ disinto_up() {
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Validate environment variables before proceeding
|
||||||
|
_validate_env_vars
|
||||||
|
|
||||||
# Parse --no-regen flag; remaining args pass through to docker compose
|
# Parse --no-regen flag; remaining args pass through to docker compose
|
||||||
local no_regen=false
|
local no_regen=false
|
||||||
local -a compose_args=()
|
local -a compose_args=()
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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_<USER_UPPER>` — derived from the agent's Forgejo username (e.g., `FORGE_TOKEN_DEV_QWEN`)
|
||||||
|
- `FORGE_PASS_<USER_UPPER>` — the agent's Forgejo password
|
||||||
|
- `ANTHROPIC_BASE_URL` (local model) or `ANTHROPIC_API_KEY` (Anthropic backend)
|
||||||
|
|
||||||
|
## Rotation
|
||||||
|
|
||||||
|
Re-running `disinto hire-an-agent <same-name>` 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_<USER_UPPER>` and `FORGE_PASS_<USER_UPPER>`
|
||||||
|
3. Restart the agent service: `docker compose restart disinto-agents-<name>`
|
||||||
|
|
||||||
### Running all 7 roles (agents-llama-all)
|
### Running all 7 roles (agents-llama-all)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,44 @@ disinto_hire_an_agent() {
|
||||||
export "${pass_var}=${user_pass}"
|
export "${pass_var}=${user_pass}"
|
||||||
fi
|
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).
|
# 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
|
# Without this, PATCH /issues/{n} {assignees:[agent]} returns 403 Forbidden and
|
||||||
# the dev-agent polls forever logging "claim lost to <none> — skipping" (see
|
# the dev-agent polls forever logging "claim lost to <none> — skipping" (see
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue