fix: bug: TOML [agents.X] section name with dash crashes load-project.sh (#862)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/smoke-init Pipeline was successful

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) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-04-16 11:55:56 +00:00
parent c63ca86a3c
commit 721d7a6077
3 changed files with 221 additions and 6 deletions

View file

@ -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