From 10c50832735be55469e198381877e1f47cf63d33 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 11 Feb 2026 18:34:50 -0500 Subject: [PATCH] Remove generic runner, consolidate to OpenClaw-only - Delete generic loop runner: compose.yml, runner/, bots/, scripts/fleet-*.sh, KEYS.md - Rewrite README.md as single OpenClaw entrypoint (merge openclaw/README.md) - Rename CLAUDE.md to AGENTS.md, create CLAUDE.md symlink - Simplify .gitignore (remove deleted path exceptions) - Fix openclaw-up.sh and openclaw-down.sh to exclude example.env from "start all" - Fix stale tail process accumulation in openclaw-tail-session.sh and openclaw-live.sh --- .gitignore | 7 +- AGENTS.md | 92 ++++++++++++++++++++++ CLAUDE.md | 116 +-------------------------- KEYS.md | 24 ------ README.md | 130 +++++++++++++++++++++++-------- bots/example.env | 31 -------- compose.yml | 23 ------ openclaw/README.md | 114 --------------------------- runner/Dockerfile | 22 ------ runner/entrypoint.sh | 87 --------------------- scripts/fleet-down.sh | 75 ------------------ scripts/fleet-logs.sh | 40 ---------- scripts/fleet-up.sh | 94 ---------------------- scripts/openclaw-down.sh | 7 +- scripts/openclaw-live.sh | 13 ++++ scripts/openclaw-tail-session.sh | 8 ++ scripts/openclaw-up.sh | 7 +- 17 files changed, 226 insertions(+), 664 deletions(-) create mode 100644 AGENTS.md mode change 100644 => 120000 CLAUDE.md delete mode 100644 KEYS.md delete mode 100644 bots/example.env delete mode 100644 compose.yml delete mode 100644 openclaw/README.md delete mode 100644 runner/Dockerfile delete mode 100755 runner/entrypoint.sh delete mode 100755 scripts/fleet-down.sh delete mode 100755 scripts/fleet-logs.sh delete mode 100755 scripts/fleet-up.sh diff --git a/.gitignore b/.gitignore index e708040..0384f97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ -runtime/ *.env -!bots/example.env !openclaw/bots/example.env -# Runtime/state generated by bot workspaces +# Runtime state +openclaw/runtime/ + +# Bot workspace generated dirs openclaw/workspaces/*/state/ openclaw/workspaces/*/memory/ openclaw/workspaces/*/research/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bed0a64 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,92 @@ +# AGENTS.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Is + +Clawdapus is a Docker-based runtime for running one or many autonomous OpenClaw bots in parallel. Each bot runs inside its own container with heartbeat scheduling, workspace-backed memory, tool execution, and cron-scheduled tasks via the `openclaw` npm CLI. + +## Common Commands + +All scripts are in `scripts/` and run from the repo root. + +```bash +bash scripts/openclaw-up.sh alpha # start a specific bot (builds + health check) +bash scripts/openclaw-up.sh # start all bots from openclaw/bots/*.env +bash scripts/openclaw-down.sh alpha # stop a specific bot +bash scripts/openclaw-down.sh # stop all + +# Observability +bash scripts/openclaw-logs.sh alpha # docker compose logs +bash scripts/openclaw-tail-session.sh alpha --with-tools # live session JSONL stream +bash scripts/openclaw-console.sh alpha # health + heartbeat + live conversation +bash scripts/openclaw-last.sh alpha # health + balance + last assistant message +bash scripts/openclaw-live.sh alpha # combined session + cron job logs + +# Run arbitrary openclaw CLI inside container +bash scripts/openclaw-cmd.sh alpha 'openclaw health --json' +bash scripts/openclaw-cmd.sh alpha 'openclaw models set openrouter/anthropic/claude-sonnet-4' +``` + +## Architecture + +### Compose Stack + +`openclaw/compose.yml` defines a single `openclaw` service using `openclaw/runner/Dockerfile` (node-based with `openclaw` npm package). The agent gateway handles heartbeat + sessions + tool execution. Cron runs inside the container to schedule periodic tasks (balance sync, opportunity scanning, cycle execution) via a workspace `crontab` file. + +### Per-Bot Isolation + +Each bot gets its own: +- **Env file**: `openclaw/bots/.env` +- **Docker Compose project**: `openclaw-` +- **State directory**: `openclaw/runtime//` +- **Workspace mount**: `BOT_REPO_PATH -> /workspace` (read/write) + +Scripts resolve bot names to env files: pass either a name (`alpha`) or explicit path (`/path/to/file.env`). + +### Container Internals + +- `openclaw.json` is generated on the host by `openclaw-up.sh` (from env vars) and bind-mounted **read-only** into the container. The bot cannot change its own heartbeat frequency, model, or scheduling. +- The openclaw npm package is made non-writable at build time (`chmod -R a-w`) to prevent runtime self-patching. +- A system heartbeat cron is installed in `/etc/cron.d/heartbeat-override` by the entrypoint — it fires `openclaw gateway call wake` at the operator-configured `OPENCLAW_HEARTBEAT_EVERY` interval. The bot cannot disable or modify this cron. +- The bot's workspace crontab (`/workspace/crontab`) is installed separately and is bot-editable. The bot manages its own task crons. +- Entrypoint (`openclaw/runner/entrypoint.sh`) runs `openclaw setup`, generates a gateway auth token (persisted at `/state/openclaw/gateway.token`), installs crons, then starts `openclaw gateway`. +- `openclaw-cmd.sh` auto-injects the gateway token before running commands. +- Session data lives at `/state/openclaw/agents/main/sessions/` as JSONL files. +- The Dockerfile patches OpenClaw's cron prompt relay to empty string (removes built-in cron reminder text from heartbeat payloads). + +### Workspace Model + +Bot workspaces live in `openclaw/workspaces//` and contain: +- `AGENTS.md` — agent instructions (mounted read-only) +- `CYCLE.md` — operator override / mission control input +- `MEMORY.md` — durable lessons +- `memory/YYYY-MM-DD.md` — daily research/strategy log +- `scripts/` — bot-specific scripts (e.g., `run_cycle.cjs`, `clob_scan_opportunities.cjs`) +- `state/` — runtime artifacts (balance.json, opportunities.json, trades.json, positions.json) + +Directories `state/`, `memory/`, `research/`, `logs/` are gitignored per workspace. + +## Adding a New Bot + +```bash +cp openclaw/bots/example.env openclaw/bots/mybot.env +# Edit openclaw/bots/mybot.env: set BOT_REPO_PATH, AGENTS_FILE_PATH, model/provider keys +# Create workspace: mkdir -p openclaw/workspaces/mybot && cp openclaw/workspaces/default/AGENTS.md openclaw/workspaces/mybot/ +bash scripts/openclaw-up.sh mybot +``` + +## Gotchas + +- **AGENTS.md is mounted read-only** inside containers. Edit the file on the host at `AGENTS_FILE_PATH`; changes appear inside the container immediately but the agent only re-reads on session/heartbeat boundaries. +- **Dockerfile patches node_modules at build time** (removes cron prompt relay text). Changing `OPENCLAW_NPM_PACKAGE` version requires verifying the regex patch in the Dockerfile still matches the new package. +- **Cron jobs log to `/workspace/state/logs/`** inside the container. The agent can read these logs and the workspace `crontab` is editable by the agent at runtime (`crontab /workspace/crontab` to reload). +- **Gateway token is auto-generated on first run** and persisted at `/state/openclaw/gateway.token`. If you delete the state directory, the token regenerates and any external references to the old token break. +- **Entrypoint uses `set -euo pipefail`** — config errors are fatal. + +## Key Conventions + +- Shell scripts use `set -euo pipefail` and the `sanitize()` function to normalize bot names to DNS-safe slugs for Docker project names. +- All timing values (intervals, delays, timeouts) are configured via env vars and validated as non-negative integers at startup. +- Periodic tasks (balance sync, scanning, cycle execution) run via cron inside the container, scheduled by the workspace `crontab` file. +- The OpenClaw npm package version is pinned via `OPENCLAW_NPM_PACKAGE` build arg (default: `openclaw@2026.2.9`). diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 8007aea..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,115 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What This Is - -Clawdapus is a Docker-based runtime for running one or many autonomous bots in parallel. It operates in two modes: - -1. **Generic Loop Runner** (`compose.yml` + `runner/`) — runs any shell command on an interval inside a container. Stateless scheduling only. -2. **OpenClaw Stack** (`openclaw/`) — full agent runtime with heartbeat scheduling, workspace-backed memory, tool execution, and cron-scheduled tasks. Runs the `openclaw` npm CLI inside a single container per bot. - -## Common Commands - -All scripts are in `scripts/` and run from the repo root. - -### Generic Loop Runner - -```bash -bash scripts/fleet-up.sh # start all bots from bots/*.env -bash scripts/fleet-up.sh alpha # start a specific bot -bash scripts/fleet-down.sh # stop all -bash scripts/fleet-logs.sh alpha # tail logs for a bot -``` - -### OpenClaw Stack - -```bash -bash scripts/openclaw-up.sh alpha # start a specific bot (builds + health check) -bash scripts/openclaw-up.sh # start all bots from openclaw/bots/*.env -bash scripts/openclaw-down.sh alpha # stop a specific bot -bash scripts/openclaw-down.sh # stop all - -# Observability -bash scripts/openclaw-logs.sh alpha # docker compose logs -bash scripts/openclaw-tail-session.sh alpha --with-tools # live session JSONL stream -bash scripts/openclaw-console.sh alpha # health + heartbeat + live conversation -bash scripts/openclaw-last.sh alpha # health + balance + last assistant message -bash scripts/openclaw-live.sh alpha # combined session + cron job logs - -# Run arbitrary openclaw CLI inside container -bash scripts/openclaw-cmd.sh alpha 'openclaw health --json' -bash scripts/openclaw-cmd.sh alpha 'openclaw models set openrouter/anthropic/claude-sonnet-4' -``` - -## Architecture - -### Two Compose Stacks - -- `compose.yml` — single `bot` service using `runner/Dockerfile` (debian-slim + bash/python/git). Entrypoint is a loop that runs `BOT_COMMAND` every `BOT_INTERVAL_SECONDS` with timeout and jitter. -- `openclaw/compose.yml` — single `openclaw` service using `openclaw/runner/Dockerfile` (node-based with `openclaw` npm package). The agent gateway handles heartbeat + sessions + tool execution. Cron runs inside the container to schedule periodic tasks (balance sync, opportunity scanning, cycle execution) via a workspace `crontab` file. - -### Per-Bot Isolation - -Each bot gets its own: -- **Env file**: `bots/.env` (generic) or `openclaw/bots/.env` (OpenClaw) -- **Docker Compose project**: `clawdapus-` or `openclaw-` -- **State directory**: `runtime//` or `openclaw/runtime//` -- **Workspace mount**: `BOT_REPO_PATH -> /workspace` (read/write) - -Scripts resolve bot names to env files: pass either a name (`alpha`) or explicit path (`/path/to/file.env`). - -### OpenClaw Container Internals - -- `openclaw.json` is generated on the host by `openclaw-up.sh` (from env vars) and bind-mounted **read-only** into the container. The bot cannot change its own heartbeat frequency, model, or scheduling. -- The openclaw npm package is made non-writable at build time (`chmod -R a-w`) to prevent runtime self-patching. -- A system heartbeat cron is installed in `/etc/cron.d/heartbeat-override` by the entrypoint — it fires `openclaw gateway call wake` at the operator-configured `OPENCLAW_HEARTBEAT_EVERY` interval. The bot cannot disable or modify this cron. -- The bot's workspace crontab (`/workspace/crontab`) is installed separately and is bot-editable. The bot manages its own task crons. -- Entrypoint (`openclaw/runner/entrypoint.sh`) runs `openclaw setup`, generates a gateway auth token (persisted at `/state/openclaw/gateway.token`), installs crons, then starts `openclaw gateway`. -- `openclaw-cmd.sh` auto-injects the gateway token before running commands. -- Session data lives at `/state/openclaw/agents/main/sessions/` as JSONL files. -- The Dockerfile patches OpenClaw's cron prompt relay to empty string (removes built-in cron reminder text from heartbeat payloads). - -### Workspace Model (OpenClaw) - -Bot workspaces live in `openclaw/workspaces//` and contain: -- `AGENTS.md` — agent instructions (mounted read-only) -- `CYCLE.md` — operator override / mission control input -- `MEMORY.md` — durable lessons -- `memory/YYYY-MM-DD.md` — daily research/strategy log -- `scripts/` — bot-specific scripts (e.g., `run_cycle.cjs`, `clob_scan_opportunities.cjs`) -- `state/` — runtime artifacts (balance.json, opportunities.json, trades.json, positions.json) - -Directories `state/`, `memory/`, `research/`, `logs/` are gitignored per workspace. - -## Adding a New Bot - -### Generic Loop Runner -```bash -cp bots/example.env bots/mybot.env -# Edit bots/mybot.env: set BOT_REPO_PATH, BOT_COMMAND, credentials -bash scripts/fleet-up.sh mybot -``` - -### OpenClaw -```bash -cp openclaw/bots/example.env openclaw/bots/mybot.env -# Edit openclaw/bots/mybot.env: set BOT_REPO_PATH, AGENTS_FILE_PATH, model/provider keys -# Create workspace: mkdir -p openclaw/workspaces/mybot && cp openclaw/workspaces/default/AGENTS.md openclaw/workspaces/mybot/ -bash scripts/openclaw-up.sh mybot -``` - -## Gotchas - -- **AGENTS.md is mounted read-only** inside OpenClaw containers. Edit the file on the host at `AGENTS_FILE_PATH`; changes appear inside the container immediately but the agent only re-reads on session/heartbeat boundaries. -- **Generic runner uses `set -u` (no `-e`)** — a failing `BOT_COMMAND` does not kill the loop; it logs the error and continues. OpenClaw entrypoint uses `set -euo pipefail` — config errors are fatal. -- **OpenClaw Dockerfile patches node_modules at build time** (removes cron prompt relay text). Changing `OPENCLAW_NPM_PACKAGE` version requires verifying the regex patch in the Dockerfile still matches the new package. -- **Cron jobs log to `/workspace/state/logs/`** inside the container. The agent can read these logs and the workspace `crontab` is editable by the agent at runtime (`crontab /workspace/crontab` to reload). -- **Gateway token is auto-generated on first run** and persisted at `/state/openclaw/gateway.token`. If you delete the state directory, the token regenerates and any external references to the old token break. - -## Key Conventions - -- Shell scripts use `set -euo pipefail` and the `sanitize()` function to normalize bot names to DNS-safe slugs for Docker project names. -- All timing values (intervals, delays, timeouts) are configured via env vars and validated as non-negative integers at startup. -- Periodic tasks (balance sync, scanning, cycle execution) run via cron inside the container, scheduled by the workspace `crontab` file. -- The OpenClaw npm package version is pinned via `OPENCLAW_NPM_PACKAGE` build arg (default: `openclaw@2026.2.9`). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/KEYS.md b/KEYS.md deleted file mode 100644 index d044350..0000000 --- a/KEYS.md +++ /dev/null @@ -1,24 +0,0 @@ -# API Keys / Secrets Checklist - -The fleet framework does not require keys by itself. -Keys depend on the command you run in `BOT_COMMAND`. - -## Usually Required - -- One LLM provider key: - - `ANTHROPIC_API_KEY` - - `OPENAI_API_KEY` - - `OPENROUTER_API_KEY` - -## Trading / Venue Credentials (if your strategy executes trades) - -- Market API credentials (exchange-specific), often: - - API key / secret / passphrase -- Wallet credential for signing (if required by venue): - - private key or managed signer config - -## Recommended Per-Bot Isolation - -- Use one `.env` file per bot under `bots/`. -- Keep separate wallet/API credentials per bot. -- Never share production keys across all bots. diff --git a/README.md b/README.md index 2c28e61..3544ca3 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,128 @@ # Clawdapus -Docker-based runtime for running one or many autonomous bots in parallel. +Docker-based runtime for running one or many autonomous [OpenClaw](https://docs.openclaw.ai) bots in parallel, each with isolated workspace and state. -Two modes: +## Features -- **Generic Loop Runner** (`compose.yml` + `runner/`) — runs any shell command on an interval inside a container. Bring your own bot logic; Clawdapus handles scheduling, timeouts, jitter, and per-bot isolation. -- **OpenClaw Stack** (`openclaw/`) — full agent runtime powered by [OpenClaw](https://docs.openclaw.ai). Heartbeat-driven scheduling, workspace-backed memory, tool execution, and cron-scheduled periodic tasks. +- Per-bot isolated OpenClaw container with cron-scheduled periodic tasks +- Immutable config — `openclaw.json` is generated on the host and bind-mounted read-only; the bot cannot change its own heartbeat frequency or model +- System heartbeat cron (`/etc/cron.d/`) fires at the operator-set interval regardless of bot behavior +- Bot-managed workspace crons for tasks the bot controls +- Workspace-backed memory and editable strategy files +- Tool/runtime execution inside the mounted workspace -## Layout +## Quick Start -- `compose.yml`: generic loop-runner compose file -- `runner/`: generic loop-runner image -- `bots/*.env`: per-bot env files for generic loop runner -- `openclaw/`: OpenClaw-specific stack, workspaces, and docs -- `scripts/`: start/stop/log helper scripts for both modes -- `runtime/`: per-bot persisted runtime state +1. Create a bot env file: -## Mode 1: Generic Loop Runner +```bash +cp openclaw/bots/example.env openclaw/bots/alpha.env +``` -Use this when you already have your own bot logic and only need scheduling/containerization. +2. Edit at minimum: -### Quick Start +- `BOT_REPO_PATH` — host path to the bot workspace +- `AGENTS_FILE_PATH` — host path to the agent instructions file +- model/provider keys (`OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`, etc.) +- heartbeat/cycle settings you want enabled -1. Create env files: +3. Start: ```bash -cp bots/example.env bots/alpha.env -cp bots/example.env bots/beta.env +bash scripts/openclaw-up.sh alpha ``` -2. Edit each env: +4. Stop: -- `BOT_REPO_PATH`: host path to mounted workspace -- `BOT_COMMAND`: command executed each cycle inside `/workspace` -- Optional: `BOT_INTERVAL_SECONDS`, `BOT_FAIL_DELAY_SECONDS`, provider keys +```bash +bash scripts/openclaw-down.sh alpha +``` -3. Start: +## Run Multiple Bots + +Create one env file per bot in `openclaw/bots/`, each with its own workspace/state paths. + +Start all: ```bash -bash scripts/fleet-up.sh +bash scripts/openclaw-up.sh ``` -4. Logs: +Stop all: ```bash -bash scripts/fleet-logs.sh alpha +bash scripts/openclaw-down.sh ``` -5. Stop: +## Operate and Observe + +Run OpenClaw CLI in-container: ```bash -bash scripts/fleet-down.sh +bash scripts/openclaw-cmd.sh alpha 'openclaw health --json' +``` + +Log streams: + +```bash +bash scripts/openclaw-logs.sh alpha # docker compose logs +bash scripts/openclaw-tail-session.sh alpha --with-tools # live session JSONL stream +bash scripts/openclaw-console.sh alpha # health + heartbeat + live conversation +bash scripts/openclaw-last.sh alpha # health + balance + last assistant message +bash scripts/openclaw-live.sh alpha # combined session + cron job logs +``` + +## Directory Layout + ``` +openclaw/ + compose.yml # Docker Compose stack definition + bots/*.env # per-bot configuration + workspaces// # bot workspace (strategy files, scripts, state) + runner/ # Dockerfile + entrypoint + runtime// # persisted runtime state (gitignored) +scripts/openclaw-*.sh # lifecycle and observability helpers +``` + +Container mounts: + +- `BOT_REPO_PATH -> /workspace` (read/write) +- `AGENTS_FILE_PATH -> /workspace/AGENTS.md` (read-only) +- `BOT_STATE_PATH -> /state` (read/write) + +## Configuration + +Core: -## Mode 2: OpenClaw Stack +- `OPENCLAW_MODEL_PRIMARY` — model identifier (e.g. `openrouter/anthropic/claude-sonnet-4`) +- `OPENCLAW_HEARTBEAT_EVERY` — heartbeat interval (e.g. `30m`) +- `OPENCLAW_HEARTBEAT_TARGET` — heartbeat target (e.g. `none`) -Use this when you want agent runtime behavior (heartbeat, memory files, tool execution, isolated sessions). +Credentials (as needed): -See `openclaw/README.md`. +- `OPENROUTER_API_KEY` / `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` +- venue credentials required by your strategy + +Cron-scheduled tasks (configured via workspace `crontab`): + +- Balance sync: `POLYMARKET_SYNC_*` +- Opportunity scan: `POLY_SCAN_*` + +See `openclaw/API_KEYS.md` for full key setup guide. + +## Adding a New Bot + +```bash +cp openclaw/bots/example.env openclaw/bots/mybot.env +# Edit: set BOT_REPO_PATH, AGENTS_FILE_PATH, model/provider keys +# Create workspace: +mkdir -p openclaw/workspaces/mybot +cp openclaw/workspaces/default/AGENTS.md openclaw/workspaces/mybot/ +bash scripts/openclaw-up.sh mybot +``` -## Notes +## Publishing Notes -- Keys are not required by the framework itself. Provide only what your bot needs in each bot env file. -- Keep credentials isolated per bot env. -- Generic loop runner logs persist under `runtime//logs`. +- Remove secrets from all `*.env` files before publishing. +- Keep `openclaw/bots/example.env` as template-only. +- Avoid committing runtime state under `openclaw/workspaces/*/state` unless intentional. diff --git a/bots/example.env b/bots/example.env deleted file mode 100644 index 536ff9f..0000000 --- a/bots/example.env +++ /dev/null @@ -1,31 +0,0 @@ -# Human-readable label used in logs. -BOT_NAME=example - -# Absolute path to the checked-out strategy repo on the host. -# Each bot can point to a different checkout/branch/repo. -BOT_REPO_PATH=/absolute/path/to/your/strategy-repo - -# Command run every cycle inside the mounted repo. -# Keep this as your own script entrypoint to avoid biasing strategy logic. -BOT_COMMAND=./run-bot.sh - -# Loop behavior -BOT_INTERVAL_SECONDS=600 -BOT_JITTER_SECONDS=15 -BOT_TIMEOUT_SECONDS=540 -BOT_FAIL_DELAY_SECONDS=30 - -# Optional: override per-bot state location on host. -# If omitted, scripts set runtime/ -# BOT_STATE_PATH=/absolute/path/to/state - -# Optional: model/provider credentials used by your own strategy command. -# ANTHROPIC_API_KEY= -# OPENAI_API_KEY= -# OPENROUTER_API_KEY= - -# Optional: exchange/trading credentials used by your own strategy command. -# POLYMARKET_PRIVATE_KEY= -# POLYMARKET_API_KEY= -# POLYMARKET_API_SECRET= -# POLYMARKET_API_PASSPHRASE= diff --git a/compose.yml b/compose.yml deleted file mode 100644 index 0502e05..0000000 --- a/compose.yml +++ /dev/null @@ -1,23 +0,0 @@ -services: - bot: - image: ${BOT_IMAGE:-clawdapus-runner:latest} - build: - context: ${BOT_BUILD_CONTEXT:-./runner} - dockerfile: ${BOT_DOCKERFILE:-Dockerfile} - restart: unless-stopped - environment: - BOT_NAME: ${BOT_NAME:-bot} - BOT_COMMAND: ${BOT_COMMAND:-echo "Set BOT_COMMAND in your bot env file"} - BOT_INTERVAL_SECONDS: ${BOT_INTERVAL_SECONDS:-600} - BOT_JITTER_SECONDS: ${BOT_JITTER_SECONDS:-0} - BOT_TIMEOUT_SECONDS: ${BOT_TIMEOUT_SECONDS:-540} - BOT_FAIL_DELAY_SECONDS: ${BOT_FAIL_DELAY_SECONDS:-30} - BOT_WORKDIR: /workspace - TZ: ${TZ:-UTC} - volumes: - - type: bind - source: ${BOT_REPO_PATH:-.} - target: /workspace - - type: bind - source: ${BOT_STATE_PATH:-./runtime/default} - target: /state diff --git a/openclaw/README.md b/openclaw/README.md deleted file mode 100644 index 186c729..0000000 --- a/openclaw/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# OpenClaw Fleet - -Containerized OpenClaw runtime for one or many bots, each with isolated workspace and state. - -All paths below are relative to the repository root. - -## Features - -- Per-bot isolated OpenClaw container with cron-scheduled periodic tasks -- Immutable config — `openclaw.json` is generated on the host and bind-mounted read-only; the bot cannot change its own heartbeat frequency or model -- System heartbeat cron (`/etc/cron.d/`) fires at the operator-set interval regardless of bot behavior -- Bot-managed workspace crons for tasks the bot controls -- Workspace-backed memory and editable strategy files -- Tool/runtime execution inside the mounted workspace - -## Directory Model - -- `openclaw/compose.yml`: OpenClaw stack definition -- `openclaw/bots/*.env`: per-bot configuration -- `openclaw/workspaces//`: bot workspace (strategy files, scripts, state) -- `scripts/openclaw-*.sh`: lifecycle and observability helpers - -Container mounts: - -- `BOT_REPO_PATH -> /workspace` (read/write) -- `AGENTS_FILE_PATH -> /workspace/AGENTS.md` (read-only) -- `BOT_STATE_PATH -> /state` (read/write) - -## Quick Start - -1. Create a bot env file: - -```bash -cp openclaw/bots/example.env openclaw/bots/alpha.env -``` - -2. Edit at minimum: - -- `BOT_REPO_PATH` -- `AGENTS_FILE_PATH` -- model/provider keys -- heartbeat/cycle settings you want enabled - -3. Start: - -```bash -bash scripts/openclaw-up.sh alpha -``` - -4. Stop: - -```bash -bash scripts/openclaw-down.sh alpha -``` - -## Operate and Observe - -Run OpenClaw CLI in-container: - -```bash -bash scripts/openclaw-cmd.sh alpha 'openclaw health --json' -``` - -Log streams: - -```bash -bash scripts/openclaw-logs.sh alpha -bash scripts/openclaw-tail-session.sh alpha --with-tools -bash scripts/openclaw-console.sh alpha -bash scripts/openclaw-last.sh alpha -bash scripts/openclaw-live.sh alpha -``` - -`openclaw-live.sh` combines assistant session stream with cron job logs. - -## Run Multiple Bots - -Create one env file per bot in `openclaw/bots/`, each with its own workspace/state paths. - -Start all: - -```bash -bash scripts/openclaw-up.sh -``` - -Stop all: - -```bash -bash scripts/openclaw-down.sh -``` - -## Configuration Overview - -Core: - -- `OPENCLAW_MODEL_PRIMARY` -- `OPENCLAW_HEARTBEAT_EVERY` -- `OPENCLAW_HEARTBEAT_TARGET` - -Credentials (as needed): - -- `OPENROUTER_API_KEY` / `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` -- venue credentials required by your strategy - -Cron-scheduled tasks (configured via workspace `crontab`): - -- Balance sync: `POLYMARKET_SYNC_*` -- Opportunity scan: `POLY_SCAN_*` - -## Publishing Notes - -- Remove secrets from all `*.env` files before publishing. -- Keep `openclaw/bots/example.env` as template-only. -- Avoid committing runtime state under `openclaw/workspaces/*/state` unless intentional. diff --git a/runner/Dockerfile b/runner/Dockerfile deleted file mode 100644 index 8cb3496..0000000 --- a/runner/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM debian:bookworm-slim - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y --no-install-recommends \ - bash \ - ca-certificates \ - coreutils \ - curl \ - git \ - jq \ - python3 \ - python3-pip \ - tini \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /workspace - -COPY entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh - -ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] diff --git a/runner/entrypoint.sh b/runner/entrypoint.sh deleted file mode 100755 index f3f857d..0000000 --- a/runner/entrypoint.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env bash -set -u - -BOT_NAME="${BOT_NAME:-bot}" -BOT_WORKDIR="${BOT_WORKDIR:-/workspace}" -BOT_COMMAND="${BOT_COMMAND:-}" -BOT_INTERVAL_SECONDS="${BOT_INTERVAL_SECONDS:-600}" -BOT_JITTER_SECONDS="${BOT_JITTER_SECONDS:-0}" -BOT_TIMEOUT_SECONDS="${BOT_TIMEOUT_SECONDS:-540}" -BOT_FAIL_DELAY_SECONDS="${BOT_FAIL_DELAY_SECONDS:-30}" -STATE_DIR="/state" -LOG_DIR="${STATE_DIR}/logs" -RUNS_LOG="${STATE_DIR}/runs.log" - -mkdir -p "${LOG_DIR}" - -now() { - date -u +"%Y-%m-%dT%H:%M:%SZ" -} - -log() { - printf '[%s] [%s] %s\n' "$(now)" "${BOT_NAME}" "$*" -} - -is_positive_int() { - [[ "$1" =~ ^[0-9]+$ ]] -} - -if [[ ! -d "${BOT_WORKDIR}" ]]; then - log "BOT_WORKDIR does not exist: ${BOT_WORKDIR}" - exit 1 -fi - -if [[ -z "${BOT_COMMAND}" ]]; then - log "BOT_COMMAND is empty. Set BOT_COMMAND in your bot env file." - exit 1 -fi - -for value in "${BOT_INTERVAL_SECONDS}" "${BOT_JITTER_SECONDS}" "${BOT_TIMEOUT_SECONDS}" "${BOT_FAIL_DELAY_SECONDS}"; do - if ! is_positive_int "${value}"; then - log "Timing env vars must be non-negative integers." - exit 1 - fi -done - -cd "${BOT_WORKDIR}" - -log "loop starting" -log "workdir=${BOT_WORKDIR} interval=${BOT_INTERVAL_SECONDS}s timeout=${BOT_TIMEOUT_SECONDS}s" - -while true; do - run_id="$(date -u +%Y%m%dT%H%M%SZ)-$RANDOM" - run_log="${LOG_DIR}/${run_id}.log" - started_at="$(now)" - - if (( BOT_JITTER_SECONDS > 0 )); then - jitter=$(( RANDOM % (BOT_JITTER_SECONDS + 1) )) - log "jitter sleep ${jitter}s" - sleep "${jitter}" - fi - - log "run start id=${run_id}" - - set +e - timeout "${BOT_TIMEOUT_SECONDS}" bash -lc "${BOT_COMMAND}" >"${run_log}" 2>&1 - status=$? - set -e - - finished_at="$(now)" - - if [[ ${status} -eq 0 ]]; then - log "run ok id=${run_id}" - elif [[ ${status} -eq 124 ]]; then - log "run timeout id=${run_id} timeout=${BOT_TIMEOUT_SECONDS}s" - else - log "run failed id=${run_id} status=${status}" - fi - - printf '{"runId":"%s","startedAt":"%s","finishedAt":"%s","status":%d,"logFile":"%s"}\n' \ - "${run_id}" "${started_at}" "${finished_at}" "${status}" "${run_log}" >> "${RUNS_LOG}" - - if [[ ${status} -ne 0 && ${BOT_FAIL_DELAY_SECONDS} -gt 0 ]]; then - sleep "${BOT_FAIL_DELAY_SECONDS}" - fi - - sleep "${BOT_INTERVAL_SECONDS}" -done diff --git a/scripts/fleet-down.sh b/scripts/fleet-down.sh deleted file mode 100755 index 4aff024..0000000 --- a/scripts/fleet-down.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -BOTS_DIR="${STACK_DIR}/bots" -COMPOSE_FILE="${STACK_DIR}/compose.yml" - -sanitize() { - local cleaned - cleaned="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed -E 's/^-+//; s/-+$//; s/-+/-/g')" - if [[ -z "${cleaned}" ]]; then - cleaned="bot" - fi - echo "${cleaned}" -} - -resolve_env_file() { - local arg="$1" - if [[ -f "$arg" ]]; then - echo "$arg" - return 0 - fi - if [[ -f "${BOTS_DIR}/${arg}.env" ]]; then - echo "${BOTS_DIR}/${arg}.env" - return 0 - fi - return 1 -} - -collect_targets() { - if [[ $# -eq 0 ]]; then - shopt -s nullglob - local files=("${BOTS_DIR}"/*.env) - shopt -u nullglob - printf '%s\n' "${files[@]}" - return 0 - fi - - local arg - for arg in "$@"; do - local env_file - env_file="$(resolve_env_file "$arg")" || { - echo "Cannot resolve bot env for: $arg" >&2 - exit 1 - } - printf '%s\n' "$env_file" - done -} - -main() { - mapfile -t env_files < <(collect_targets "$@") - - if [[ ${#env_files[@]} -eq 0 ]]; then - echo "No bots to stop." - exit 0 - fi - - local env_file - for env_file in "${env_files[@]}"; do - local bot_id - bot_id="$(basename "${env_file}" .env)" - local project - project="clawdapus-$(sanitize "${bot_id}")" - - echo "Stopping ${bot_id} (project=${project})" - docker compose \ - --project-name "${project}" \ - --file "${COMPOSE_FILE}" \ - --env-file "${env_file}" \ - down - done -} - -main "$@" diff --git a/scripts/fleet-logs.sh b/scripts/fleet-logs.sh deleted file mode 100755 index c72008a..0000000 --- a/scripts/fleet-logs.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -BOTS_DIR="${STACK_DIR}/bots" -COMPOSE_FILE="${STACK_DIR}/compose.yml" - -sanitize() { - local cleaned - cleaned="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed -E 's/^-+//; s/-+$//; s/-+/-/g')" - if [[ -z "${cleaned}" ]]; then - cleaned="bot" - fi - echo "${cleaned}" -} - -if [[ $# -ne 1 ]]; then - echo "Usage: $(basename "$0") " - exit 1 -fi - -input="$1" -if [[ -f "${input}" ]]; then - env_file="${input}" -elif [[ -f "${BOTS_DIR}/${input}.env" ]]; then - env_file="${BOTS_DIR}/${input}.env" -else - echo "Cannot resolve env file: ${input}" >&2 - exit 1 -fi - -bot_id="$(basename "${env_file}" .env)" -project="clawdapus-$(sanitize "${bot_id}")" - -docker compose \ - --project-name "${project}" \ - --file "${COMPOSE_FILE}" \ - --env-file "${env_file}" \ - logs -f diff --git a/scripts/fleet-up.sh b/scripts/fleet-up.sh deleted file mode 100755 index 09f8d56..0000000 --- a/scripts/fleet-up.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -BOTS_DIR="${STACK_DIR}/bots" -COMPOSE_FILE="${STACK_DIR}/compose.yml" - -usage() { - cat <&2 - echo "Copy bots/example.env to bots/.env first." >&2 - exit 1 - fi - printf '%s\n' "${files[@]}" - return 0 - fi - - local arg - for arg in "$@"; do - local env_file - env_file="$(resolve_env_file "$arg")" || { - echo "Cannot resolve bot env for: $arg" >&2 - exit 1 - } - printf '%s\n' "$env_file" - done -} - -main() { - if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then - usage - exit 0 - fi - - mapfile -t env_files < <(collect_targets "$@") - - local env_file - for env_file in "${env_files[@]}"; do - local bot_id - bot_id="$(basename "${env_file}" .env)" - local slug - slug="$(sanitize "${bot_id}")" - local project - project="clawdapus-${slug}" - local state_dir - state_dir="${STACK_DIR}/runtime/${slug}" - mkdir -p "${state_dir}" - - echo "Starting ${bot_id} (project=${project})" - BOT_STATE_PATH="${state_dir}" docker compose \ - --project-name "${project}" \ - --file "${COMPOSE_FILE}" \ - --env-file "${env_file}" \ - up -d --build - done -} - -main "$@" diff --git a/scripts/openclaw-down.sh b/scripts/openclaw-down.sh index 8143fef..6472cd4 100755 --- a/scripts/openclaw-down.sh +++ b/scripts/openclaw-down.sh @@ -32,8 +32,13 @@ resolve_env_file() { collect_targets() { if [[ $# -eq 0 ]]; then shopt -s nullglob - local files=("${BOTS_DIR}"/*.env) + local all=("${BOTS_DIR}"/*.env) shopt -u nullglob + local files=() + for f in "${all[@]}"; do + [[ "$(basename "$f")" == "example.env" ]] && continue + files+=("$f") + done printf '%s\n' "${files[@]}" return 0 fi diff --git a/scripts/openclaw-live.sh b/scripts/openclaw-live.sh index dd31181..95645f7 100755 --- a/scripts/openclaw-live.sh +++ b/scripts/openclaw-live.sh @@ -48,6 +48,19 @@ trap cleanup EXIT INT TERM ) & pids+=($!) +# Kill stale tail -F processes on cron log files from previous invocations. +docker compose \ + --project-name "${project}" \ + --file "${COMPOSE_FILE}" \ + --env-file "${env_file}" \ + exec -T openclaw bash -c ' + for f in /proc/[0-9]*/cmdline; do + pid="${f#/proc/}"; pid="${pid%%/*}" + cmd="$(tr "\0" " " < "$f" 2>/dev/null)" || continue + [[ "$cmd" == *tail*-F*.log* ]] && kill "$pid" 2>/dev/null || true + done + ' 2>/dev/null || true + ( docker compose \ --project-name "${project}" \ diff --git a/scripts/openclaw-tail-session.sh b/scripts/openclaw-tail-session.sh index cf129c7..2d87e2e 100755 --- a/scripts/openclaw-tail-session.sh +++ b/scripts/openclaw-tail-session.sh @@ -83,6 +83,14 @@ if [[ -z "${SESSION_FILE}" || ! -f "${SESSION_FILE}" ]]; then exit 1 fi +# Kill stale tail -F processes on session JSONL files from previous invocations. +# docker exec orphans these when the host-side script is interrupted. +for f in /proc/[0-9]*/cmdline; do + pid="${f#/proc/}"; pid="${pid%%/*}" + cmd="$(tr '\0' ' ' < "$f" 2>/dev/null)" || continue + [[ "$cmd" == *tail*-F*.jsonl* ]] && kill "$pid" 2>/dev/null || true +done + echo "Tailing session: ${SESSION_FILE}" if [[ "${WITH_TOOLS:-0}" == "1" ]]; then diff --git a/scripts/openclaw-up.sh b/scripts/openclaw-up.sh index 339e6d1..2a98641 100755 --- a/scripts/openclaw-up.sh +++ b/scripts/openclaw-up.sh @@ -32,8 +32,13 @@ resolve_env_file() { collect_targets() { if [[ $# -eq 0 ]]; then shopt -s nullglob - local files=("${BOTS_DIR}"/*.env) + local all=("${BOTS_DIR}"/*.env) shopt -u nullglob + local files=() + for f in "${all[@]}"; do + [[ "$(basename "$f")" == "example.env" ]] && continue + files+=("$f") + done if [[ ${#files[@]} -eq 0 ]]; then echo "No OpenClaw bot env files found in ${BOTS_DIR}" >&2 echo "Copy openclaw/bots/example.env to openclaw/bots/.env first." >&2