Compare commits
3 commits
de318d3511
...
5bbd9487c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bbd9487c9 | ||
|
|
daa165b0a0 | ||
|
|
aa418fbd7a |
5 changed files with 41 additions and 101 deletions
|
|
@ -2,7 +2,7 @@ FROM debian:bookworm-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
bash curl git jq tmux python3 python3-pip openssh-client ca-certificates age shellcheck procps gosu \
|
bash curl git jq tmux python3 python3-pip openssh-client ca-certificates age shellcheck procps gosu \
|
||||||
&& pip3 install --break-system-packages networkx tomlkit \
|
&& pip3 install --break-system-packages networkx \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Pre-built binaries (copied from docker/agents/bin/)
|
# Pre-built binaries (copied from docker/agents/bin/)
|
||||||
|
|
|
||||||
|
|
@ -123,11 +123,6 @@ _generate_local_model_services() {
|
||||||
context: .
|
context: .
|
||||||
dockerfile: docker/agents/Dockerfile
|
dockerfile: docker/agents/Dockerfile
|
||||||
image: disinto/agents:\${DISINTO_IMAGE_TAG:-latest}
|
image: disinto/agents:\${DISINTO_IMAGE_TAG:-latest}
|
||||||
# Rebuild on every up (#887): without this, \`docker compose up -d --force-recreate\`
|
|
||||||
# reuses the cached image and silently keeps running stale docker/agents/ code
|
|
||||||
# even after the repo is updated. \`pull_policy: build\` makes Compose rebuild
|
|
||||||
# the image on every up; BuildKit layer cache makes unchanged rebuilds fast.
|
|
||||||
pull_policy: build
|
|
||||||
container_name: disinto-agents-${service_name}
|
container_name: disinto-agents-${service_name}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|
@ -448,9 +443,6 @@ COMPOSEEOF
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: docker/agents/Dockerfile
|
dockerfile: docker/agents/Dockerfile
|
||||||
# Rebuild on every up (#887): makes docker/agents/ source changes reach this
|
|
||||||
# container without a manual \`docker compose build\`. Cache-fast when clean.
|
|
||||||
pull_policy: build
|
|
||||||
container_name: disinto-agents-llama
|
container_name: disinto-agents-llama
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|
@ -501,9 +493,6 @@ COMPOSEEOF
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: docker/agents/Dockerfile
|
dockerfile: docker/agents/Dockerfile
|
||||||
# Rebuild on every up (#887): makes docker/agents/ source changes reach this
|
|
||||||
# container without a manual \`docker compose build\`. Cache-fast when clean.
|
|
||||||
pull_policy: build
|
|
||||||
container_name: disinto-agents-llama-all
|
container_name: disinto-agents-llama-all
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles: ["agents-llama-all"]
|
profiles: ["agents-llama-all"]
|
||||||
|
|
|
||||||
|
|
@ -535,10 +535,7 @@ EOF
|
||||||
local interval="${poll_interval:-60}"
|
local interval="${poll_interval:-60}"
|
||||||
echo " Writing [agents.${section_name}] to ${toml_file}..."
|
echo " Writing [agents.${section_name}] to ${toml_file}..."
|
||||||
python3 -c '
|
python3 -c '
|
||||||
import sys
|
import sys, re, pathlib
|
||||||
import tomlkit
|
|
||||||
import re
|
|
||||||
import pathlib
|
|
||||||
|
|
||||||
toml_path = sys.argv[1]
|
toml_path = sys.argv[1]
|
||||||
section_name = sys.argv[2]
|
section_name = sys.argv[2]
|
||||||
|
|
@ -551,39 +548,38 @@ poll_interval = sys.argv[7]
|
||||||
p = pathlib.Path(toml_path)
|
p = pathlib.Path(toml_path)
|
||||||
text = p.read_text()
|
text = p.read_text()
|
||||||
|
|
||||||
# Step 1: Remove any commented-out [agents.X] blocks (they cause parse issues)
|
# Build the new section
|
||||||
# Match # [agents.section_name] followed by lines that are not section headers
|
new_section = f"""
|
||||||
# Use negative lookahead to stop before a real section header (# [ or [)
|
[agents.{section_name}]
|
||||||
commented_pattern = rf"(?:^|\n)# \[agents\.{re.escape(section_name)}\](?:\n(?!# \[|\[)[^\n]*)*"
|
base_url = "{base_url}"
|
||||||
text = re.sub(commented_pattern, "", text, flags=re.DOTALL)
|
model = "{model}"
|
||||||
|
api_key = "sk-no-key-required"
|
||||||
|
roles = ["{role}"]
|
||||||
|
forge_user = "{agent_name}"
|
||||||
|
compact_pct = 60
|
||||||
|
poll_interval = {poll_interval}
|
||||||
|
"""
|
||||||
|
|
||||||
# Step 2: Parse TOML with tomlkit (preserves comments and formatting)
|
# Check if section already exists and replace it
|
||||||
try:
|
pattern = rf"\[agents\.{re.escape(section_name)}\][^\[]*"
|
||||||
doc = tomlkit.parse(text)
|
if re.search(pattern, text):
|
||||||
except Exception as e:
|
text = re.sub(pattern, new_section.strip() + "\n", text)
|
||||||
print(f"Error: Invalid TOML in {toml_path}: {e}", file=sys.stderr)
|
else:
|
||||||
sys.exit(1)
|
# Remove commented-out example [agents.llama] block if present
|
||||||
|
text = re.sub(
|
||||||
|
r"\n# Local-model agents \(optional\).*?(?=\n# \[mirrors\]|\n\[mirrors\]|\Z)",
|
||||||
|
"",
|
||||||
|
text,
|
||||||
|
flags=re.DOTALL,
|
||||||
|
)
|
||||||
|
# Append before [mirrors] if it exists, otherwise at end
|
||||||
|
mirrors_match = re.search(r"\n(# )?\[mirrors\]", text)
|
||||||
|
if mirrors_match:
|
||||||
|
text = text[:mirrors_match.start()] + "\n" + new_section + text[mirrors_match.start():]
|
||||||
|
else:
|
||||||
|
text = text.rstrip() + "\n" + new_section
|
||||||
|
|
||||||
# Step 3: Ensure agents table exists
|
p.write_text(text)
|
||||||
if "agents" not in doc:
|
|
||||||
doc.add("agents", tomlkit.table())
|
|
||||||
|
|
||||||
# Step 4: Update the specific agent section
|
|
||||||
doc["agents"][section_name] = {
|
|
||||||
"base_url": base_url,
|
|
||||||
"model": model,
|
|
||||||
"api_key": "sk-no-key-required",
|
|
||||||
"roles": [role],
|
|
||||||
"forge_user": agent_name,
|
|
||||||
"compact_pct": 60,
|
|
||||||
"poll_interval": int(poll_interval),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Step 5: Serialize back to TOML (preserves comments)
|
|
||||||
output = tomlkit.dumps(doc)
|
|
||||||
|
|
||||||
# Step 6: Write back
|
|
||||||
p.write_text(output)
|
|
||||||
' "$toml_file" "$section_name" "$local_model" "$model" "$agent_name" "$role" "$interval"
|
' "$toml_file" "$section_name" "$local_model" "$model" "$agent_name" "$role" "$interval"
|
||||||
|
|
||||||
echo " Agent config written to TOML"
|
echo " Agent config written to TOML"
|
||||||
|
|
|
||||||
|
|
@ -97,38 +97,6 @@ EOF
|
||||||
[[ "$output" == *'dockerfile: docker/agents/Dockerfile'* ]]
|
[[ "$output" == *'dockerfile: docker/agents/Dockerfile'* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "local-model agent service emits pull_policy: build so docker compose up rebuilds on source change (#887)" {
|
|
||||||
# Without pull_policy: build, `docker compose up -d --force-recreate` reuses
|
|
||||||
# the cached `disinto/agents:latest` image and silently runs stale
|
|
||||||
# docker/agents/entrypoint.sh even after the repo is updated. `pull_policy:
|
|
||||||
# build` forces a rebuild on every up; BuildKit layer cache makes unchanged
|
|
||||||
# rebuilds near-instant. The alternative was requiring every operator to
|
|
||||||
# remember `--build` on every invocation, which was the bug that prompted
|
|
||||||
# #887 (2h of debugging a fix that was merged but never reached the container).
|
|
||||||
cat > "${FACTORY_ROOT}/projects/test.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 = "qwen"
|
|
||||||
api_key = "sk-no-key-required"
|
|
||||||
roles = ["dev"]
|
|
||||||
forge_user = "dev-qwen2"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
run bash -c "
|
|
||||||
set -euo pipefail
|
|
||||||
source '${ROOT}/lib/generators.sh'
|
|
||||||
_generate_local_model_services '${FACTORY_ROOT}/docker-compose.yml'
|
|
||||||
cat '${FACTORY_ROOT}/docker-compose.yml'
|
|
||||||
"
|
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
|
||||||
[[ "$output" == *'pull_policy: build'* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "local-model agent service keys FORGE_BOT_USER to forge_user even when it differs from service name (#849)" {
|
@test "local-model agent service keys FORGE_BOT_USER to forge_user even when it differs from service name (#849)" {
|
||||||
# Exercise the case the issue calls out: two agents in the same factory
|
# Exercise the case the issue calls out: two agents in the same factory
|
||||||
# whose service names are identical (`[agents.llama]`) but whose
|
# whose service names are identical (`[agents.llama]`) but whose
|
||||||
|
|
|
||||||
|
|
@ -420,38 +420,25 @@ EOF
|
||||||
local unchanged=0
|
local unchanged=0
|
||||||
|
|
||||||
for op in "${operations[@]}"; do
|
for op in "${operations[@]}"; do
|
||||||
# Parse operation: category|field|subkey|file|envvar (5 fields for bots/runner)
|
# Parse operation: category|field|file|key (4 fields for most, 5 for bots/runner)
|
||||||
# or category|field|file|envvar (4 fields for forge/woodpecker/chat)
|
IFS='|' read -r category field file key <<< "$op"
|
||||||
local category field subkey file envvar=""
|
local source_value=""
|
||||||
local field_count
|
|
||||||
field_count="$(printf '%s' "$op" | awk -F'|' '{print NF}')"
|
|
||||||
|
|
||||||
if [ "$field_count" -eq 5 ]; then
|
if [ "$file" = "$env_file" ]; then
|
||||||
# 5 fields: category|role|subkey|file|envvar
|
source_value="${!key:-}"
|
||||||
IFS='|' read -r category field subkey file envvar <<< "$op"
|
|
||||||
else
|
else
|
||||||
# 4 fields: category|field|file|envvar
|
# Source from sops-decrypted env
|
||||||
IFS='|' read -r category field file envvar <<< "$op"
|
source_value="$(printf '%s' "$sops_env" | grep "^${key}=" | sed "s/^${key=}//" || true)"
|
||||||
subkey="$field" # For 4-field ops, field is the vault key
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Determine Vault path and key based on category
|
# Determine Vault path and key based on category
|
||||||
local vault_path=""
|
local vault_path=""
|
||||||
local vault_key="$subkey"
|
local vault_key="$key"
|
||||||
local source_value=""
|
|
||||||
|
|
||||||
if [ "$file" = "$env_file" ]; then
|
|
||||||
# Source from environment file (envvar contains the variable name)
|
|
||||||
source_value="${!envvar:-}"
|
|
||||||
else
|
|
||||||
# Source from sops-decrypted env (envvar contains the variable name)
|
|
||||||
source_value="$(printf '%s' "$sops_env" | grep "^${envvar}=" | sed "s/^${envvar}=//" || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$category" in
|
case "$category" in
|
||||||
bots)
|
bots)
|
||||||
vault_path="disinto/bots/${field}"
|
vault_path="disinto/bots/${field}"
|
||||||
vault_key="$subkey"
|
vault_key="$field"
|
||||||
;;
|
;;
|
||||||
forge)
|
forge)
|
||||||
vault_path="disinto/shared/forge"
|
vault_path="disinto/shared/forge"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue