Skill: joe7921/agent-deploy (version 2.4.0)

--- name: agent-deploy description: Deploy a new isolated OpenClaw agent with its own Telegram bot, workspace, and session storage. Use when user asks to create a new agent, add a new bot, or set up multi-agent routing. Uses openclaw config set for safe config updates with doctor validation and auto-rollback. --- # Agent Deploy & Isolation Skill ## WHEN TO USE THIS SKILL Use this skill when the user says ANY of the following (or similar): - "deploy a new agent" - "add a new agent" - "create a new agent" - "set up a new bot" - "bind a bot to a new agent" - "add a telegram bot" - "list agents" or "show agents" - "remove agent" or "delete agent" ## WHAT YOU NEED FROM THE USER Before running any deploy script, you MUST collect these two values from the user: | Required | Example | How to get it | |----------|---------|---------------| | **agentId** | `research` | Ask: "What should I name this agent?" (lowercase, no spaces, no special chars) | | **botToken** | `123456:ABC-xyz` | Ask: "What is the Telegram Bot Token?" (user gets this from @BotFather) | If the user provides both in their message, proceed immediately. If the user is missing one or both, ask for the missing value(s) before proceeding. ## HOW TO EXECUTE ### Action: DEPLOY a new agent Run this exact command, replacing `<agentId>` and `<botToken>` with the user's values: ```bash bash {baseDir}/scripts/deploy.sh <agentId> <botToken> ``` **Example:** If user says "deploy agent called research with token 123456:ABCdef": ```bash bash {baseDir}/scripts/deploy.sh research 123456:ABCdef ``` After the script finishes: - If output contains "SUCCESS": tell the user the agent is deployed. - If output contains "CONFLICT": tell the user the agent already exists. - If output contains "ERROR": tell the user what went wrong. - If output contains "ROLLING BACK": tell the user the change was safely reverted. DO NOT run `systemctl restart` unless the script output explicitly says to. The script handles hot-reload automatically for channels and bindings. ### Action: LIST all agents ```bash bash {baseDir}/scripts/list.sh ``` Show the output table to the user as-is. ### Action: REMOVE an agent Run this exact command, replacing `<agentId>`: ```bash bash {baseDir}/scripts/remove.sh <agentId> ``` **Example:** If user says "remove the research agent": ```bash bash {baseDir}/scripts/remove.sh research ``` ## STRICT RULES ??DO NOT VIOLATE 1. **NEVER edit openclaw.json directly.** Do not use `write`, `edit`, `apply_patch`, or any file editing tool on `openclaw.json`. The deploy script uses `openclaw config set` which is the only safe way. 2. **NEVER skip the pre-flight check.** Always run the full deploy.sh script. Do not try to run individual `openclaw config set` commands yourself. 3. **NEVER change the agentId format.** It must be lowercase letters, numbers, and hyphens only. No spaces, no uppercase, no special characters. 4. **NEVER deploy without a valid bot token.** The token must match the format: `digits:alphanumeric` (e.g., `[REDACTED_SECRET]`). 5. **NEVER modify the main agent.** The `remove.sh` script refuses to remove the main agent. Do not try to work around this. ## WHAT THE SCRIPT DOES AUTOMATICALLY You do NOT need to do any of these manually. The script handles everything: - Creates isolated workspace at `~/.openclaw/workspace-<agentId>/` - Adds agent to `agents.list` with safe defaults: - `tools.deny: ["gateway"]` (agent cannot modify core config) - `sandbox.mode: "non-main"` (non-main sessions are sandboxed) - `sandbox.scope: "agent"` (one container per agent) - `sandbox.workspaceAccess: "none"` (sandbox cannot access host workspace) - Adds routing binding: `<agentId>` -> `telegram:<agentId>` - Adds Telegram account with the bot token - Validates with `openclaw doctor` - Auto-rollbacks on any failure - Merges API keys from BOTH global config (`openclaw.json` auth.profiles) AND main agent's auth-profiles.json - Migrates from single-bot to multi-account mode if needed ## TROUBLESHOOTING If the user says the new bot is not responding after deploy: 1. First, check logs: `journalctl --user -u openclaw-gateway --no-pager -n 20` 2. Look for `[telegram] [<agentId>] starting provider` in logs 3. If NOT found, restart: `systemctl --user restart openclaw-gateway` 4. If still not working, run: `bash {baseDir}/scripts/list.sh` to verify config ## ENVIRONMENT VARIABLES These are optional. The scripts use sensible defaults: | Variable | Default | Description | |----------|---------|-------------| | `OPENCLAW_CONFIG_PATH` | `~/.openclaw/openclaw.json` | Custom config file path | | `OPENCLAW_BIN` | `openclaw` | Custom openclaw CLI path |

path: scripts/deploy.sh
content: #!/bin/bash
# agent-deploy v2.1: Deploy a new isolated OpenClaw agent
# Uses openclaw config set + doctor validation + auto-rollback
set -euo pipefail

AGENT_ID="${1:?Usage: deploy.sh <agentId> <botToken> [workspace_path]}"
BOT_TOKEN="${2:?Usage: deploy.sh <agentId> <botToken> [workspace_path]}"
WORKSPACE="${3:-$HOME/.openclaw/workspace-$AGENT_ID}"
CONFIG="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}"
BACKUP="$HOME/.openclaw/openclaw.json.pre-$AGENT_ID"
OC="${OPENCLAW_BIN:-openclaw}"
HELPER="$(dirname "$0")/deploy_helper.py"

echo "[agent-deploy] v2.4"
echo "  Agent:     $AGENT_ID"
echo "  [REDACTED_SECRET]"
echo "  Workspace: $WORKSPACE"
echo "  Config:    $CONFIG"
echo ""

# Validate token format
if ! echo "$BOT_TOKEN" | grep -qP '^[0-9]+:[A-Za-z0-9_-]+$'; then
    echo "ERROR: Invalid bot token format (expected digits:alphanumeric)"
    exit 1
fi

if [ ! -f "$CONFIG" ]; then
    echo "ERROR: Config not found: $CONFIG"
    exit 1
fi

# [1/8] Pre-flight checks
echo "[1/8] Pre-flight checks..."
PREFLIGHT=$(python3 "$HELPER" preflight "$AGENT_ID" "$BOT_TOKEN" 2>&1) || {
    echo "$PREFLIGHT"
    exit 1
}
echo "  $PREFLIGHT"

# [2/8] Backup
echo "[2/8] Backup..."
cp "$CONFIG" "$BACKUP"
echo "  $BACKUP"

# [3/8] Create workspace
echo "[3/8] Create workspace..."
mkdir -p "$WORKSPACE/memory" "$WORKSPACE/output" "$WORKSPACE/skills"
if [ ! -f "$WORKSPACE/SOUL.md" ]; then
    printf '# Agent\n\nYou are a helpful AI assistant.\n' > "$WORKSPACE/SOUL.md"
    echo "  Created default SOUL.md"
fi
echo "  $WORKSPACE"


# [4/8] Merge auth profiles (global + main agent)
echo "[4/8] Merge auth profiles..."
python3 "$HELPER" merge-auth "$AGENT_ID"
if [ $? -ne 0 ]; then
    echo "  WARNING: Auth merge failed. New agent may not have API keys."
    echo "  Run manually: openclaw agents add $AGENT_ID"
fi

# [5/8] Update agents.list
echo "[5/8] Update agents.list..."
NEW_LIST=$(python3 "$HELPER" gen-agents-list "$AGENT_ID" "$WORKSPACE")
$OC config set agents.list "$NEW_LIST" || {
    echo "ERROR: Failed to set agents.list. Rolling back..."
    cp "$BACKUP" "$CONFIG"
    exit 1
}
echo "  Added $AGENT_ID"

# [6/8] Update bindings
echo "[6/8] Update bindings..."
NEW_BINDINGS=$(python3 "$HELPER" gen-bindings "$AGENT_ID")
$OC config set bindings "$NEW_BINDINGS" || {
    echo "ERROR: Failed to set bindings. Rolling back..."
    cp "$BACKUP" "$CONFIG"
    exit 1
}
echo "  Added $AGENT_ID -> telegram:$AGENT_ID"

# [7/8] Update telegram accounts
echo "[7/8] Update telegram accounts..."
# Handle single-bot to multi-account migration
if echo "$PREFLIGHT" | grep -q "MIGRATE_SINGLE_BOT"; then
    echo "  Migrating from single-bot to multi-account mode..."
    OLD_ACCT=$(python3 "$HELPER" migrate-single-bot)
    $OC config set channels.telegram.accounts.default "$OLD_ACCT" || true
    $OC config unset channels.telegram.botToken 2>/dev/null || true
    $OC config unset channels.telegram.dmPolicy 2>/dev/null || true
fi
ACCT_JSON="{\"botToken\": \"$BOT_TOKEN\", \"dmPolicy\": \"pairing\"}"
$OC config set "channels.telegram.accounts.$AGENT_ID" "$ACCT_JSON" || {
    echo "ERROR: Failed to set telegram account. Rolling back..."
    cp "$BACKUP" "$CONFIG"
    exit 1
}
echo "  Added account $AGENT_ID"

# [8/8] Doctor validation
echo "[8/8] Validating..."
if ! $OC doctor 2>/dev/null; then
    echo ""
    echo "VALIDATION FAILED. Rolling back..."
    cp "$BACKUP" "$CONFIG"
    echo "Rolled back to: $BACKUP"
    exit 1
fi
echo "  Doctor check passed"

echo ""
echo "SUCCESS: Agent '$AGENT_ID' deployed"
echo "  Sandbox:  mode=non-main, scope=agent, workspaceAccess=none"
echo "  Tools:    deny=[gateway]"
echo "  Telegram: account=$AGENT_ID"
echo ""
echo "NOTE: channels/bindings hot-reload automatically."
echo "      If not responding, restart: systemctl --user restart openclaw-gateway"


path: scripts/deploy_helper.py
content: #!/usr/bin/env python3
"""agent-deploy helper: pre-flight checks + config generation"""
import json, sys, os

def main():
    action = sys.argv[1]
    config_path = os.environ.get("OPENCLAW_CONFIG_PATH",
                                 os.path.expanduser("~/.openclaw/openclaw.json"))

    with open(config_path) as f:
        config = json.load(f)

    if action == "preflight":
        agent_id = sys.argv[2]
        bot_token = sys.argv[3]
        errors = []
        for a in config.get("agents", {}).get("list", []):
            if a.get("id") == agent_id:
                errors.append(f"Agent '{agent_id}' already in agents.list")
        for b in config.get("bindings", []):
            if b.get("agentId") == agent_id:
                errors.append(f"Binding for '{agent_id}' already exists")
            m = b.get("match", {})
            if m.get("channel") == "telegram" and m.get("accountId") == agent_id and b.get("agentId") != agent_id:
                errors.append(f"AccountId '{agent_id}' bound to '{b['agentId']}'")
        accts = config.get("channels", {}).get("telegram", {}).get("accounts", {})
        if agent_id in accts:
            errors.append(f"Telegram account '{agent_id}' already exists")
        for k, v in accts.items():
            if v.get("botToken") == bot_token:
                errors.append(f"Token already used by account '{k}'")
        # Check if still in single-bot mode
        tg = config.get("channels", {}).get("telegram", {})
        if "botToken" in tg and "accounts" not in tg:
            print("MIGRATE_SINGLE_BOT")
        if errors:
            for e in errors:
                print(f"CONFLICT: {e}", file=sys.stderr)
            sys.exit(1)
        print("OK")

    elif action == "gen-agents-list":
        agent_id = sys.argv[2]
        workspace = sys.argv[3]
        lst = config.get("agents", {}).get("list", [])
        lst.append({
            "id": agent_id,
            "workspace": workspace,
            "tools": {"deny": ["gateway"]},
            "sandbox": {
                "mode": "non-main",
                "scope": "agent",
                "workspaceAccess": "none"
            }
        })
        print(json.dumps(lst))

    elif action == "gen-bindings":
        agent_id = sys.argv[2]
        lst = config.get("bindings", [])
        lst.append({
            "agentId": agent_id,
            "match": {"channel": "telegram", "accountId": agent_id}
        })
        print(json.dumps(lst))

    elif action == "migrate-single-bot":
        tg = config.get("channels", {}).get("telegram", {})
        old_token = tg.get("botToken", "")
        old_dm = tg.get("dmPolicy", "pairing")
        if old_token:
            acct = json.dumps({"botToken": old_token, "dmPolicy": old_dm})
            print(acct)
        else:
            print("{}")

    elif action == "list":
        agents = config.get("agents", {}).get("list", [])
        bindings = config.get("bindings", [])
        accounts = config.get("channels", {}).get("telegram", {}).get("accounts", {})
        fmt = "{:<12} {:<3} {:<36} {:<15} {:<12} {}"
        print("Configured Agents")
        print()
        print(fmt.format("ID", "", "Workspace", "Tools Deny", "TG Account", "Bot Token"))
        print("-" * 100)
        for a in agents:
            aid = a.get("id", "?")
            ws = a.get("workspace", "?")
            deny = ",".join(a.get("tools", {}).get("deny", [])) or "none"
            sb_mode = a.get("sandbox", {}).get("mode", "off")
            tg_acct = "none"
            for b in bindings:
                if b.get("agentId") == aid:
                    m = b.get("match", {})
                    if m.get("channel") == "telegram":
                        tg_acct = m.get("accountId", "?")
            [REDACTED_SECRET], {}).get("botToken", "N/A")
            t_short = token[:10] + "..." if len(token) > 10 else token
            ws_ok = "OK" if os.path.isdir(ws) else "NO"
            print(fmt.format(aid, ws_ok, ws, deny, tg_acct, t_short))
            print(fmt.format("", "", "", "sandbox=" + sb_mode, "", ""))
        print()
        print(f"Total: {len(agents)} agents | Config: {config_path}")

    elif action == "gen-remove-agents":
        agent_id = sys.argv[2]
        lst = config.get("agents", {}).get("list", [])
        lst = [a for a in lst if a.get("id") != agent_id]
        print(json.dumps(lst))


    elif action == "merge-auth":
        agent_id = sys.argv[2]
        agent_auth_path = os.path.expanduser(
            f"~/.openclaw/agents/{agent_id}/agent/auth-profiles.json"
        )
        main_auth_path = os.path.expanduser(
            "~/.openclaw/agents/main/agent/auth-profiles.json"
        )

        # Start with empty auth
        merged = {"version": 1, "profiles": {}, "lastGood": {}, "usageStats": {}}

        # Source 1: Global auth from openclaw.json
        global_profiles = config.get("auth", {}).get("profiles", {})
        for pid, pdata in global_profiles.items():
            merged["profiles"][pid] = pdata
            provider = pdata.get("provider", pid.split(":")[0])
            merged["lastGood"][provider] = pid
            merged["usageStats"][pid] = {"lastUsed": 0, "errorCount": 0}
            print(f"  [global] {pid}")

        # Source 2: Main agent per-agent auth
        if os.path.isfile(main_auth_path):
            with open(main_auth_path) as f2:
                main_auth = json.load(f2)
            for pid, pdata in main_auth.get("profiles", {}).items():
                if pid not in merged["profiles"]:
                    merged["profiles"][pid] = pdata
                    provider = pdata.get("provider", pid.split(":")[0])
                    merged["lastGood"][provider] = pid
                    merged["usageStats"][pid] = {"lastUsed": 0, "errorCount": 0}
                    print(f"  [main]   {pid}")

        # Write to agent auth dir
        os.makedirs(os.path.dirname(agent_auth_path), exist_ok=True)
        with open(agent_auth_path, "w") as f2:
            json.dump(merged, f2, indent=2)

        print(f"  Total: {len(merged['profiles'])} profiles -> {agent_auth_path}")

    elif action == "gen-remove-bindings":
        agent_id = sys.argv[2]
        lst = config.get("bindings", [])
        lst = [b for b in lst if b.get("agentId") != agent_id]
        print(json.dumps(lst))

if __name__ == "__main__":
    main()


path: scripts/list.sh
content: #!/bin/bash
# agent-deploy v2.1: List all configured agents
HELPER="$(dirname "$0")/deploy_helper.py"
python3 "$HELPER" list


path: scripts/remove.sh
content: #!/bin/bash
# agent-deploy v2.1: Remove an OpenClaw agent
set -euo pipefail

AGENT_ID="${1:?Usage: remove.sh <agentId>}"
CONFIG="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}"
BACKUP="$HOME/.openclaw/openclaw.json.pre-remove-$AGENT_ID"
OC="${OPENCLAW_BIN:-openclaw}"
HELPER="$(dirname "$0")/deploy_helper.py"

if [ "$AGENT_ID" = "main" ]; then
    echo "ERROR: Cannot remove the main agent!"
    exit 1
fi

echo "[agent-deploy] Remove: $AGENT_ID"

if [ ! -f "$CONFIG" ]; then
    echo "ERROR: Config not found: $CONFIG"
    exit 1
fi

cp "$CONFIG" "$BACKUP"
echo "  Backup: $BACKUP"

echo "  Removing from agents.list..."
NEW_LIST=$(python3 "$HELPER" gen-remove-agents "$AGENT_ID")
$OC config set agents.list "$NEW_LIST"

echo "  Removing from bindings..."
NEW_BINDINGS=$(python3 "$HELPER" gen-remove-bindings "$AGENT_ID")
$OC config set bindings "$NEW_BINDINGS"

echo "  Removing telegram account..."
$OC config unset "channels.telegram.accounts.$AGENT_ID" 2>/dev/null || true

echo "  Validating..."
if ! $OC doctor 2>/dev/null; then
    echo "VALIDATION FAILED. Rolling back..."
    cp "$BACKUP" "$CONFIG"
    exit 1
fi

echo ""
echo "SUCCESS: Agent '$AGENT_ID' removed"
echo "  Workspace NOT deleted: $HOME/.openclaw/workspace-$AGENT_ID"
echo "  Channels/bindings hot-reload automatically."


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)