Skip to content

Leanware-io/openalma

Repository files navigation

OpenAlma — AI Delivery Copilot

An OpenClaw agent that monitors Slack, ClickUp, and Google Drive for a single client project. It runs on a 30-minute heartbeat, detects unanswered messages, flags missing tasks, due-date risks, ambiguities, and Sentry events — then proposes concrete actions in Slack for human confirmation. It can create tasks and spawn claude code through ACP.

Architecture

Two Docker containers on one bridge network (openclaw-net):

Container Role Exposed
openclaw-gateway Custom image (built from gateway/Dockerfile on top of ghcr.io/openclaw/openclaw) bundling the OpenClaw gateway and the Claude Code CLI. ACP launches claude as a local stdio subprocess of the gateway — no docker exec, no docker socket. 127.0.0.1:18789 (SSH tunnel only)
api-proxy FastAPI app. Holds third-party credentials (ClickUp, Slack, Google) and exposes them to the gateway over the internal network. Internal only (expose: 8000)

The two containers are credential-isolated: the gateway never sees third-party API tokens, and the proxy never sees Anthropic/Slack bot tokens or GH_TOKEN. See CLAUDE.md for the full isolation model.

The proxy serves one project per deployment. The non-secret shape (ClickUp list ids, Slack channel allowlist, team id) lives in project-config.json at the repo root — gitignored, mounted into the container.

Coding agent

The gateway image bundles a full coding toolchain so the agent can do real engineering work in the team's repos, not just answer questions:

  • Claude Code CLI (@anthropic-ai/claude-code) — launched as a local stdio subprocess by ACP (acpx-config.json). Same process tree as the gateway, no docker exec mux.
  • GitHub CLI (gh) — for opening pull requests, reading issues/PR comments, and inspecting CI status. Authenticated via GH_TOKEN / GITHUB_TOKEN from .env.openclaw.
  • Git, ripgrep, fd-find, python3, uv, ruff, mypy — baked into gateway/Dockerfile so the agent has the basics without bootstrapping per session.

Workspace and worktrees

The host directory ./repos/ is bind-mounted into the container at /workspace/repos. The agent works the repositories there and creates per-session git worktrees under /tmp/sessions/$SESSION_ID so concurrent sessions don't step on each other's branches. The cwd: /workspace/repos in openclaw.json resolves to this mount.

./repos/ is gitignored — it's a working area, not committed content.

Pull request flow

Once a repo is cloned into ./repos/<name>, the agent can:

  1. Create a branch in an isolated worktree.
  2. Make and commit changes.
  3. Push with git push (uses GH_TOKEN).
  4. Open a PR via gh pr create against the repo's default branch.
  5. Read review comments via gh pr view / gh api and iterate.

The agent never force-pushes, never pushes to main/master, and never merges its own PRs — those are explicit red lines in gateway/agent-rules.md.

Coding rules: global vs per-repo

Two layers, in priority order:

  1. Repo-specific CLAUDE.md (highest priority) — branch naming, commit style, test commands, pre-PR checklist. Lives inside each cloned repo. The agent reads it at session start.
  2. Global gateway/agent-rules.md — repo-agnostic operating rules (worktree isolation, absolute restrictions, PR flow). Bind-mounted read-only over /home/node/.claude/CLAUDE.md in the container.

To change global rules, edit gateway/agent-rules.md on the host and docker compose --env-file .env.openclaw restart openclaw-gateway — no rebuild needed.

To change per-repo rules, commit a CLAUDE.md to that repository.

Prerequisites

  • Docker Engine and Docker Compose v2
  • SSH access to the VM
  • An Anthropic API key (required even if you plan to use a Team/Pro plan via OAuth — see step 7)
  • Slack bot + app tokens, ClickUp token, Google service account key
  • A GitHub PAT for the agent's git+gh workflow. Scopes: repo (clone/push private repos), workflow (trigger CI), and read:org if your repos live under an org. Classic PAT or a fine-grained token with equivalent permissions on the target repos.

Quick start

1. Environment files

cp .env.openclaw.example .env.openclaw
cp .env.api.example      .env.api

Fill in real values. Generate secrets:

openssl rand -hex 32   # → OPENCLAW_GATEWAY_TOKEN in .env.openclaw
openssl rand -hex 32   # → API_PROXY_KEY in both .env.openclaw AND .env.api (same value)

GH_TOKEN / GITHUB_TOKEN go in .env.openclaw — they're used by the claude-code agent (which now runs in the same container as the gateway) to clone, push, and open PRs.

2. Project config

cp project-config.example.json project-config.json

Edit project-config.json and fill in:

  • clickup.lists — map of list name → ClickUp list id (at minimum the backlog list used for creating tasks)
  • clickup.team_id — ClickUp workspace id
  • clickup.default_assignees — optional default assignees for created tasks
  • clickup.custom_id_prefix — optional. If your ClickUp workspace uses custom task IDs (e.g. PROJ-1234), set this to the prefix (e.g. "PROJ") so the proxy passes custom_task_ids=true for matching ids. Leave null to always use native ClickUp ids.
  • slack.channels — map of channel name → Slack channel id for every channel the agent should read

This file is gitignored. Do not put secrets in it — tokens live in .env.api.

3. Google service account key

Drop your service account JSON at ./google-key.json for Google Drive integration (or adjust the mount in docker-compose.yml and the GOOGLE_KEY_PATH value in .env.api if you rename it).

4. Hardcoded Slack channel ID in openclaw.json

OpenClaw doesn't support ${VAR} substitution for the Slack channel allowlist in openclaw.json. Edit openclaw/openclaw.json (around line 39) and set the channel ID where the agent should respond to @mentions:

"channels": {
    "<alma-slack-channel-id>": {
        "enabled": true,
        "requireMention": true
    }
}

Only this channels block is hardcoded — other ${SLACK_CHANNEL_ID} references in the same file (bindings, heartbeat) are resolved from the container's environment at runtime, so leave those as-is.

5. Clone the agent repositories

cd repos
git clone <repo_url>

This host directory is bind-mounted into the gateway at /workspace/repos and is where the agent works the repos and creates per-session worktrees.

6. Customize the agent workspace

The agent prompts in openclaw/workspace/ define all project-specific agent behavior — the product description, who the team and client are, how to assign tasks, what the cadence looks like, the workspace's Slack slug, and so on. The files committed to this repo are a template with dummy data; you must edit them before going live or the agent will respond using the placeholders.

File What lives here Must-customize
AGENTS.md Operating manual: modes, memory split, red lines, task creation guidelines "What this project is" section (one paragraph describing your product); example wiki entity / concept lists
USER.md People (DM, client, team), ClickUp + Slack IDs, cadence, Slack workspace slug, assignment heuristics Everything — names, emails, ClickUp IDs, Slack user IDs, workspace slug
MEMORY.md Curated long-term memory: deliveries, client decisions, known bugs, team patterns Replace template entries with real seed context (the agent will keep this updated during heartbeats)
IDENTITY.md Agent identity (name, Slack handle) Slack handle if you renamed the bot from @alma
SOUL.md Voice, source-citation rule, propose-never-decide doctrine Usually leave as-is; tweak language defaults if needed
TOOLS.md API proxy endpoint reference + memory-wiki tool reference Usually leave as-is; reflects the api-proxy surface
HEARTBEAT.md Per-cycle playbook (every 30 min) Usually leave as-is; tweak triggers / thresholds if your project needs different ones

Minimum to do before the first heartbeat:

  1. Open openclaw/workspace/USER.md and replace every dummy name, email, ClickUp ID, Slack user ID, and the your-workspace Slack slug with your real values. The agent uses the Slack-id lists to detect unanswered client messages — if these are wrong, that trigger fires incorrectly.
  2. Open openclaw/workspace/AGENTS.md and replace the "What this project is" placeholder paragraph with a real description of the product.
  3. Optionally seed openclaw/workspace/MEMORY.md with starting context (the agent keeps it updated during heartbeats — fine to leave the template at first and let it fill in over time).
  4. The hardcoded Slack channel ID in openclaw/openclaw.json (step 4 above) must be the channel where the agent receives @mentions.

After editing workspace files, restart the gateway:

docker compose --env-file .env.openclaw restart openclaw-gateway

(Workspace files are bind-mounted, so a restart is enough — no rebuild needed.)

7. Bring it up

docker compose --env-file .env.openclaw up -d --build

The first run builds the custom gateway image (Dockerfile in gateway/); subsequent runs reuse the cached image until you rebuild.

8. Anthropic auth: API key vs Team/Pro OAuth

There are two independent Anthropic consumers inside openclaw-gateway:

  1. OpenClaw's main agent (heartbeat, Slack-driven sessions) — uses ANTHROPIC_API_KEY from .env.openclaw. Always required.
  2. The claude subprocess that ACP launches for coding sessions — should use OAuth credentials from a Pro/Team plan to avoid burning API credits on long edits.

If you have a Pro/Team plan, attach it once after the gateway is up:

docker compose --env-file .env.openclaw exec -it openclaw-gateway claude /login

OAuth credentials persist in the claude-home named volume across rebuilds. Do not remove ANTHROPIC_API_KEY from .env.openclaw — OpenClaw's heartbeat agent still needs it.

Why this works: when both are present, claude normally prefers ANTHROPIC_API_KEY over OAuth. acpx-config.json launches the subprocess with env -u ANTHROPIC_API_KEY claude ... to strip the variable for that one process, so the heartbeat keeps using the API key while the coding subprocess uses OAuth.

Verify the subprocess is actually using OAuth:

docker compose --env-file .env.openclaw exec openclaw-gateway \
  sh -c 'env -u ANTHROPIC_API_KEY claude --print "/status"'

Expect apiKeySource: "claude.ai" (or similar) and a non-null email. If you see apiKeySource: "ANTHROPIC_API_KEY", OAuth didn't take — re-run /login.

9. Initialize the memory wiki (once)

docker compose --env-file .env.openclaw exec openclaw-gateway openclaw wiki init
docker compose --env-file .env.openclaw exec openclaw-gateway openclaw wiki status

Optionally seed the wiki with initial project context:

docker compose --env-file .env.openclaw exec openclaw-gateway \
  openclaw wiki apply synthesis "Project Overview" \
  --body "<one or two sentences describing your product, the team building it, and the development stage>"

docker compose --env-file .env.openclaw exec openclaw-gateway openclaw wiki compile

The agent will populate the wiki automatically during heartbeats. Manual seeding is optional.

10. Verify

# Gateway → api-proxy connectivity
docker compose --env-file .env.openclaw exec openclaw-gateway curl http://api-proxy:8000/health

# Authenticated endpoint
docker compose --env-file .env.openclaw exec openclaw-gateway \
  sh -c 'curl -H "X-API-Key: $API_PROXY_KEY" http://api-proxy:8000/protected'

Confirm credential isolation between the two containers — third-party tokens must not leak into the gateway, and gateway/agent secrets must not leak into the proxy:

# Should be EMPTY — gateway/agent secrets must not reach api-proxy
docker compose --env-file .env.openclaw exec api-proxy env \
  | grep -E 'SLACK_BOT|SLACK_APP|ANTHROPIC|GH_TOKEN|GITHUB_TOKEN'

# Should be EMPTY — third-party creds must not reach the gateway
docker compose --env-file .env.openclaw exec openclaw-gateway env \
  | grep -E 'CLICKUP_TOKEN|SLACK_TOKEN|GOOGLE_KEY_PATH|SENTRY_TOKEN'

# Should print the shared key — both sides need API_PROXY_KEY
docker compose --env-file .env.openclaw exec openclaw-gateway env | grep API_PROXY_KEY

The first heartbeat fires ~30 min after the gateway starts. It pulls 7 days of history as a backfill.

Common commands

All commands use --env-file .env.openclaw.

# Up / down
docker compose --env-file .env.openclaw up -d
docker compose --env-file .env.openclaw down

# Restart gateway (after editing openclaw.json or workspace prompts)
docker compose --env-file .env.openclaw restart openclaw-gateway

# Recreate gateway (after editing .env.openclaw — restart won't reload env vars)
docker compose --env-file .env.openclaw up -d openclaw-gateway

# Recreate api-proxy (after editing project-config.json or .env.api)
docker compose --env-file .env.openclaw up -d --force-recreate api-proxy

# Rebuild api-proxy (after changing code in api/)
docker compose --env-file .env.openclaw build api-proxy
docker compose --env-file .env.openclaw up -d api-proxy

# Rebuild gateway image (after editing gateway/Dockerfile or gateway/agent-rules.md)
docker compose --env-file .env.openclaw build openclaw-gateway
docker compose --env-file .env.openclaw up -d openclaw-gateway

# Logs
docker compose --env-file .env.openclaw logs -f openclaw-gateway
docker compose --env-file .env.openclaw logs --tail=100 api-proxy

# Wiki health
docker compose --env-file .env.openclaw exec openclaw-gateway openclaw wiki status
docker compose --env-file .env.openclaw exec openclaw-gateway openclaw plugins list | grep memory-wiki

Project structure

.
├── docker-compose.yml
├── acpx-config.json                       ← ACP agent config (claude as local subprocess)
├── .env.openclaw                          ← gateway + agent secrets (gitignored)
├── .env.api                               ← api-proxy secrets (gitignored)
├── .env.openclaw.example
├── .env.api.example
├── project-config.json                          ← project config: list/channel ids (gitignored)
├── project-config.example.json                  ← template
├── google-key.json                        ← Google service account key (gitignored)
├── gateway/                               ← merged gateway image
│   ├── Dockerfile                         ← FROM openclaw + claude-code + tooling
│   ├── agent-rules.md                     ← global CLAUDE.md for claude-code sessions
│   └── .dockerignore
├── repos/                                 ← agent's worktree workspace, mounted at /workspace/repos (gitignored)
├── openclaw/                              ← mounted as /home/node/.openclaw
│   ├── openclaw.json                      ← gateway config (hardcoded channel ID for allowlist)
│   └── workspace/                         ← agent prompts and runtime state
│       ├── AGENTS.md                      ← operating manual
│       ├── SOUL.md                        ← voice and doctrine
│       ├── USER.md                        ← people, IDs, cadence, workspace slug
│       ├── MEMORY.md                      ← curated long-term project memory
│       ├── IDENTITY.md                    ← agent identity (@alma)
│       ├── TOOLS.md                       ← tool reference
│       ├── HEARTBEAT.md                   ← per-cycle playbook
│       ├── memory/                        ← cycle state (runtime)
│       └── wiki/                          ← memory-wiki vault
├── api/
│   ├── Dockerfile
│   ├── main.py                            ← FastAPI app
│   ├── config.py                          ← loads project-config.json + env vars
│   ├── deps.py                            ← CONFIG singleton + RequireAuth
│   ├── routers/
│   │   ├── clickup.py                     ← /clickup/recent, /clickup/tasks, /clickup/tasks/{id}
│   │   ├── slack.py                       ← /slack/recent, /slack/thread, /slack/message
│   │   └── gdrive.py                      ← /gdrive/recent, /gdrive/docs/{id}
│   └── services/
│       ├── clickup.py
│       ├── slack.py
│       ├── gdrive.py
│       └── timestamps.py
├── CLAUDE.md
└── README.md

Environment files

File Read by Contains
.env.openclaw Compose (--env-file) + openclaw-gateway (env_file:) Gateway port/token, Anthropic key, Slack bot/app tokens, SLACK_CHANNEL_ID, API_PROXY_KEY (client copy), GH_TOKEN / GITHUB_TOKEN for the agent
.env.api api-proxy (env_file:) only API_PROXY_KEY (server copy), CLICKUP_TOKEN, SLACK_TOKEN, GOOGLE_KEY_PATH

API_PROXY_KEY lives in both files with the same value — it's the shared interface secret. Everything else stays on one side only.

Resources

Security

  • Secrets live only in .env.openclaw and .env.api, both gitignored.
  • Project config (project-config.json) is also gitignored — only the sanitized project-config.example.json is committed.
  • The gateway binds to 127.0.0.1 — remote access via SSH tunnel only.
  • The api-proxy has no host binding — only containers on openclaw-net can reach it.
  • The agent never messages clients directly. All proposals require human confirmation in Slack.

About

Autonomous AI Agent that codes, creates tasks and has full context of the project

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors