Compare commits
4 commits
37ee38f59a
...
a3eb33ccf7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3eb33ccf7 | ||
|
|
53a1fe397b | ||
| 9248c533d4 | |||
|
|
721d7a6077 |
4 changed files with 225 additions and 9 deletions
|
|
@ -1789,7 +1789,6 @@ _validate_env_vars() {
|
||||||
while IFS='|' read -r service_name forge_user base_url _api_key; do
|
while IFS='|' read -r service_name forge_user base_url _api_key; do
|
||||||
[ -n "$service_name" ] || continue
|
[ -n "$service_name" ] || continue
|
||||||
[ -n "$forge_user" ] || continue
|
[ -n "$forge_user" ] || continue
|
||||||
[ -n "$base_url" ] || continue
|
|
||||||
|
|
||||||
# Derive variable names (user -> USER_UPPER)
|
# Derive variable names (user -> USER_UPPER)
|
||||||
local user_upper
|
local user_upper
|
||||||
|
|
@ -1809,7 +1808,7 @@ _validate_env_vars() {
|
||||||
errors=$((errors + 1))
|
errors=$((errors + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check backend URL or API key
|
# Check backend URL or API key (conditional based on base_url presence)
|
||||||
if [ -n "$base_url" ]; then
|
if [ -n "$base_url" ]; then
|
||||||
# Local model: needs ANTHROPIC_BASE_URL
|
# Local model: needs ANTHROPIC_BASE_URL
|
||||||
if [ -z "${env_vars[ANTHROPIC_BASE_URL]:-}" ]; then
|
if [ -z "${env_vars[ANTHROPIC_BASE_URL]:-}" ]; then
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,29 @@ disinto_hire_an_agent() {
|
||||||
echo "Usage: disinto hire-an-agent <agent-name> <role> [--formula <path>] [--local-model <url>] [--model <name>] [--poll-interval <seconds>]" >&2
|
echo "Usage: disinto hire-an-agent <agent-name> <role> [--formula <path>] [--local-model <url>] [--model <name>] [--poll-interval <seconds>]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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-<name>` 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
|
shift 2
|
||||||
|
|
||||||
# Parse flags
|
# Parse flags
|
||||||
|
|
@ -239,8 +262,10 @@ disinto_hire_an_agent() {
|
||||||
# Local model agent: write ANTHROPIC_BASE_URL
|
# Local model agent: write ANTHROPIC_BASE_URL
|
||||||
local backend_var="ANTHROPIC_BASE_URL"
|
local backend_var="ANTHROPIC_BASE_URL"
|
||||||
local backend_val="$local_model"
|
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
|
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"
|
echo " ${backend_var} updated"
|
||||||
else
|
else
|
||||||
printf '%s=%s\n' "$backend_var" "$backend_val" >> "$env_file"
|
printf '%s=%s\n' "$backend_var" "$backend_val" >> "$env_file"
|
||||||
|
|
|
||||||
|
|
@ -129,20 +129,26 @@ agents = cfg.get('agents', {})
|
||||||
for name, config in agents.items():
|
for name, config in agents.items():
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
continue
|
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
|
# Emit variables in uppercase with the agent name
|
||||||
if 'base_url' in config:
|
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:
|
if 'model' in config:
|
||||||
print(f'AGENT_{name.upper()}_MODEL={config[\"model\"]}')
|
print(f'AGENT_{safe}_MODEL={config[\"model\"]}')
|
||||||
if 'api_key' in config:
|
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:
|
if 'roles' in config:
|
||||||
roles = ' '.join(config['roles']) if isinstance(config['roles'], list) else config['roles']
|
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:
|
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:
|
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
|
" "$_PROJECT_TOML" 2>/dev/null) || true
|
||||||
|
|
||||||
if [ -n "$_AGENT_VARS" ]; then
|
if [ -n "$_AGENT_VARS" ]; then
|
||||||
|
|
|
||||||
186
tests/lib-load-project.bats
Normal file
186
tests/lib-load-project.bats
Normal file
|
|
@ -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" <<EOF
|
||||||
|
name = "test"
|
||||||
|
repo = "test-owner/test-repo"
|
||||||
|
forge_url = "http://localhost:3000"
|
||||||
|
|
||||||
|
[agents.dev-qwen2]
|
||||||
|
base_url = "http://10.10.10.1:8081"
|
||||||
|
model = "unsloth/Qwen3.5-35B-A3B"
|
||||||
|
api_key = "sk-no-key-required"
|
||||||
|
roles = ["dev"]
|
||||||
|
forge_user = "dev-qwen2"
|
||||||
|
compact_pct = 60
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run bash -c "
|
||||||
|
set -euo pipefail
|
||||||
|
source '${ROOT}/lib/load-project.sh' '$TOML'
|
||||||
|
echo \"BASE=\${AGENT_DEV_QWEN2_BASE_URL:-MISSING}\"
|
||||||
|
echo \"MODEL=\${AGENT_DEV_QWEN2_MODEL:-MISSING}\"
|
||||||
|
echo \"ROLES=\${AGENT_DEV_QWEN2_ROLES:-MISSING}\"
|
||||||
|
echo \"FORGE_USER=\${AGENT_DEV_QWEN2_FORGE_USER:-MISSING}\"
|
||||||
|
echo \"COMPACT=\${AGENT_DEV_QWEN2_COMPACT_PCT:-MISSING}\"
|
||||||
|
"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"BASE=http://10.10.10.1:8081"* ]]
|
||||||
|
[[ "$output" == *"MODEL=unsloth/Qwen3.5-35B-A3B"* ]]
|
||||||
|
[[ "$output" == *"ROLES=dev"* ]]
|
||||||
|
[[ "$output" == *"FORGE_USER=dev-qwen2"* ]]
|
||||||
|
[[ "$output" == *"COMPACT=60"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "dashless [agents.*] section name still works" {
|
||||||
|
cat > "$TOML" <<EOF
|
||||||
|
name = "test"
|
||||||
|
repo = "test-owner/test-repo"
|
||||||
|
forge_url = "http://localhost:3000"
|
||||||
|
|
||||||
|
[agents.llama]
|
||||||
|
base_url = "http://10.10.10.1:8081"
|
||||||
|
model = "qwen"
|
||||||
|
api_key = "sk-no-key-required"
|
||||||
|
roles = ["dev"]
|
||||||
|
forge_user = "dev-llama"
|
||||||
|
compact_pct = 60
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run bash -c "
|
||||||
|
set -euo pipefail
|
||||||
|
source '${ROOT}/lib/load-project.sh' '$TOML'
|
||||||
|
echo \"BASE=\${AGENT_LLAMA_BASE_URL:-MISSING}\"
|
||||||
|
echo \"MODEL=\${AGENT_LLAMA_MODEL:-MISSING}\"
|
||||||
|
"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"BASE=http://10.10.10.1:8081"* ]]
|
||||||
|
[[ "$output" == *"MODEL=qwen"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "multiple dashes in [agents.*] name all normalized" {
|
||||||
|
cat > "$TOML" <<EOF
|
||||||
|
name = "test"
|
||||||
|
repo = "test-owner/test-repo"
|
||||||
|
forge_url = "http://localhost:3000"
|
||||||
|
|
||||||
|
[agents.review-qwen-3b]
|
||||||
|
base_url = "http://10.10.10.1:8082"
|
||||||
|
model = "qwen-3b"
|
||||||
|
api_key = "sk-no-key-required"
|
||||||
|
roles = ["review"]
|
||||||
|
forge_user = "review-qwen-3b"
|
||||||
|
compact_pct = 60
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run bash -c "
|
||||||
|
set -euo pipefail
|
||||||
|
source '${ROOT}/lib/load-project.sh' '$TOML'
|
||||||
|
echo \"BASE=\${AGENT_REVIEW_QWEN_3B_BASE_URL:-MISSING}\"
|
||||||
|
"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"BASE=http://10.10.10.1:8082"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "hire-agent rejects dash-starting agent name" {
|
||||||
|
run bash -c "
|
||||||
|
FACTORY_ROOT='${ROOT}' \
|
||||||
|
FORGE_URL='http://127.0.0.1:1' \
|
||||||
|
FORGE_TOKEN=x \
|
||||||
|
bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
source \"\${FACTORY_ROOT}/lib/hire-agent.sh\"
|
||||||
|
disinto_hire_an_agent -foo dev
|
||||||
|
'
|
||||||
|
"
|
||||||
|
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"invalid agent name"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "hire-agent rejects uppercase agent name" {
|
||||||
|
run bash -c "
|
||||||
|
FACTORY_ROOT='${ROOT}' \
|
||||||
|
FORGE_URL='http://127.0.0.1:1' \
|
||||||
|
FORGE_TOKEN=x \
|
||||||
|
bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
source \"\${FACTORY_ROOT}/lib/hire-agent.sh\"
|
||||||
|
disinto_hire_an_agent DevQwen dev
|
||||||
|
'
|
||||||
|
"
|
||||||
|
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"invalid agent name"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "hire-agent rejects underscore agent name" {
|
||||||
|
run bash -c "
|
||||||
|
FACTORY_ROOT='${ROOT}' \
|
||||||
|
FORGE_URL='http://127.0.0.1:1' \
|
||||||
|
FORGE_TOKEN=x \
|
||||||
|
bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
source \"\${FACTORY_ROOT}/lib/hire-agent.sh\"
|
||||||
|
disinto_hire_an_agent dev_qwen dev
|
||||||
|
'
|
||||||
|
"
|
||||||
|
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"invalid agent name"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "hire-agent rejects trailing dash agent name" {
|
||||||
|
run bash -c "
|
||||||
|
FACTORY_ROOT='${ROOT}' \
|
||||||
|
FORGE_URL='http://127.0.0.1:1' \
|
||||||
|
FORGE_TOKEN=x \
|
||||||
|
bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
source \"\${FACTORY_ROOT}/lib/hire-agent.sh\"
|
||||||
|
disinto_hire_an_agent dev- dev
|
||||||
|
'
|
||||||
|
"
|
||||||
|
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"invalid agent name"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "hire-agent rejects consecutive-dash agent name" {
|
||||||
|
run bash -c "
|
||||||
|
FACTORY_ROOT='${ROOT}' \
|
||||||
|
FORGE_URL='http://127.0.0.1:1' \
|
||||||
|
FORGE_TOKEN=x \
|
||||||
|
bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
source \"\${FACTORY_ROOT}/lib/hire-agent.sh\"
|
||||||
|
disinto_hire_an_agent dev--qwen dev
|
||||||
|
'
|
||||||
|
"
|
||||||
|
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"invalid agent name"* ]]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue