Article · 2026-03-03

Beyond Manual Configuration: Engineering AI Agent Cluster Setup

I run a local cluster of 5 AI agents—a main orchestrator, a blog writer, a development assistant, a smart home hub, and a communications assistant—all managed by the OpenClaw platform. All configurations lived in the runtime directory ~/.openclaw/, manually edited. The "source configuration" in the repository was merely a remnant from my Docker days. Today, when I wanted to batch-switch models, I hit the wall: the manual approach was unmanageable.

How Bad Was the Problem

Switching models required changes in 8 places across 2 JSON files:

Missing even one creates inconsistencies. I missed the cron job entry, and the daily writing task kept running on the old model.

The deeper issue was configuration drift. The config/openclaw.json in the repository still listed three non-existent agents (Analyzer, Scanner, Writer) and outdated model names like google/gemini-3-pro. The repository had lost its role as source of truth.

The Approach: Declarative Templates + Automated Deployment

The goal: the Git repository becomes the sole source of truth, and a single command safely synchronizes configurations to runtime.

The refactored project structure:

Hephaestus/
├── config/
│   ├── openclaw.json        # Config template (secrets use placeholders)
│   ├── cron-jobs.json       # Cron job declarations (no runtime state)
│   ├── SOUL.md              # Agent persona definitions
│   └── HEARTBEAT.md         # Daily workflow
├── secrets/
│   └── .env                 # All secrets (gitignored)
├── scripts/
│   ├── deploy.sh            # One-click deployment
│   └── ctl.sh               # Service management

Configuration Templating

Transforming the runtime openclaw.json into a template meant replacing sensitive information with environment variable placeholders:

{
  "channels": {
    "discord": {
      "enabled": true,
      "token": "${DISCORD_BOT_TOKEN}"
    }
  },
  "gateway": {
    "auth": {
      "mode": "token",
      "token": "${OPENCLAW_GATEWAY_TOKEN}"
    }
  }
}

Two types of fields were also stripped:

The template describes only "what I want the configuration to be," independent of environment or runtime artifacts.

Cron Jobs: Removing Hardcoded Models

The daily-articles cron job had a hardcoded model in its payload:

{
  "payload": {
    "kind": "agentTurn",
    "message": "读取 SOUL.md...",
    "model": "google-gemini-cli/gemini-3-pro-preview"
  }
}

This meant the cron job would use the old model even after updating the agent's default. The fix: remove the model field from the template and let it inherit from the agent's configuration.

Isolated Secret Management

Values replaced by placeholders are stored in secrets/.env, excluded via .gitignore:

OPENCLAW_GATEWAY_TOKEN=7fd8cd94...
DISCORD_BOT_TOKEN=MTQ3Njc3...
ANTHROPIC_API_KEY=sk-ant-api03-...

The Deploy Script: Merge, Not Overwrite

deploy.sh is the workflow core. It doesn't simply copy the template over—that would destroy runtime state. It performs an intelligent merge.

Key steps:

# 1. Load secrets
source "$REPO_DIR/secrets/.env"

# 2. Placeholder substitution
GENERATED=$(cat "$TEMPLATE" | sed \
  -e "s|\${DISCORD_BOT_TOKEN}|${DISCORD_BOT_TOKEN}|g" \
  -e "s|\${OPENCLAW_GATEWAY_TOKEN}|${OPENCLAW_GATEWAY_TOKEN}|g"
)

# 3. Preserve runtime meta/wizard fields
FINAL_CONFIG=$(python3 -c "
import sys, json
with open('$RUNTIME_CONFIG') as f:
    runtime = json.load(f)
generated = json.loads(sys.stdin.read())
for key in ('meta', 'wizard'):
    if key in runtime:
        generated[key] = runtime[key]
json.dump(generated, sys.stdout, indent=2, ensure_ascii=False)
" <<< "$GENERATED")

The cron job merge is more refined, reading declarative definitions from the template and state plus timestamps from the runtime, matching by id:

FINAL_CRON=$(python3 -c "
import sys, json
with open('$CRON_TEMPLATE') as f:
    template = json.load(f)
with open('$RUNTIME_CRON') as f:
    runtime = json.load(f)

runtime_state = {}
for job in runtime.get('jobs', []):
    if 'state' in job:
        runtime_state[job['id']] = job['state']

for job in template.get('jobs', []):
    if job['id'] in runtime_state:
        job['state'] = runtime_state[job['id']]

json.dump(template, sys.stdout, indent=2, ensure_ascii=False)
")

Deployment doesn't reset cron job timers. If a task is scheduled to run in 3 hours, it still runs in 3 hours after deployment—no countdown reset.

The final two steps are restart and health check:

# Restart gateway
launchctl kickstart -k "gui/$(id -u)/ai.openclaw.gateway"

# Wait for the service to come up
for i in $(seq 1 15); do
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 \
    "http://127.0.0.1:18789/__openclaw__/canvas/" 2>/dev/null || echo "000")
  if [[ "$HTTP_CODE" != "000" ]]; then
    echo "Gateway is up (HTTP $HTTP_CODE)"
    break
  fi
  sleep 2
done

Two parameters enable common workflows:

Service Management: Unified Entry Point

Previously, every operation required verbose launchctl commands. Now, ctl.sh wraps all common operations:

./scripts/ctl.sh status    # Full overview
./scripts/ctl.sh restart   # Restart gateway
./scripts/ctl.sh tail      # Real-time logs
./scripts/ctl.sh cron      # Cron job overview

The status command outputs a comprehensive dashboard:

=== OpenClaw Status ===

Gateway:
  Service:  LOADED (pid=36397)
  HTTP health: OK (HTTP 200)
  Port:     18789

Agents:
  main            model=minimax-portal/MiniMax-M2.5-highspeed [DEFAULT]
  hephaestus      model=minimax-portal/MiniMax-M2.5-highspeed
  dev             model=minimax-portal/MiniMax-M2.5-highspeed
  home            model=minimax-portal/MiniMax-M2.5-highspeed
  comms           model=minimax-portal/MiniMax-M2.5-highspeed

Cron Jobs:
  ✓ daily-articles         agent=hephaestus   last=ok   next=in 5h38min
  ✓ hacker-news-daily      agent=main         last=-    next=in 9h58min
  ✓ quant-auto-evolve      agent=main         last=-    next=in 14min

All agent models and cron job schedules, visible at a glance.

Results

The requirement—"switch all models from Gemini to MiniMax M2.5 Highspeed"—now takes two steps:

  1. Edit config/openclaw.json to change the model name
  2. Run ./scripts/deploy.sh

Done in 10 seconds, with every change tracked in Git. Or one command:

./scripts/deploy.sh --set-model minimax-portal/MiniMax-M2.5-highspeed

All agents, all heartbeats, everything updated.

Configuration templating, secret separation, and intelligent merging solved a real operational problem: consistency at scale. The pattern works because it separates concerns—repository holds intent, runtime holds state—and automates the merge.

© 2026 Yuxu Ge ·