Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@

## Project Overview

OpenClaw Orchestrator (Claworc) manages multiple OpenClaw instances in Kubernetes or Docker.
OpenClaw Orchestrator (Claworc) manages multiple AI agent instances in Kubernetes or Docker — OpenClaw by
default, plus Hermes, NanoClaw, or any custom image implementing the agent shim contract (`docs/shim.md`).
Each instance runs in its own container/pod and allows users easy access to a Chromium browser & terminal
for collaboration with the agent.

The project consists of the following components:
* Control Plane (Golang backend and React frontend) with dashboard, VNC client for Chromium, Terminal, Logs and other useful stuff.
* Agent image with OpenClaw installed. It is compatible with both ARM64 and AMD64 architectures.
* Agent images (`claworc/openclaw`, `claworc/hermes`, `claworc/nanoclaw`, plus a copy-me template). Compatible with both ARM64 and AMD64 architectures.
* Helm chart for deployment to Kubernetes.

## Repository Structure

- `agent/` - Base docker image with OpenClaw instance (`claworc/openclaw`) and images with various browsers `claworc/<browser>-browser`
- `agent/` - Agent docker images (`agent/openclaw`, `agent/hermes`, `agent/nanoclaw`, `agent/template`) and browser images `claworc/<browser>-browser`
- `control-plane/` - Main application (Go backend + React frontend)
- `main.go` - Entry point, Chi router, embedded SPA serving
- `internal/` - Go packages (config, database, handlers, middleware, orchestrator, sshproxy, sshterminal)
- `frontend/` - React TypeScript frontend (npm/Vite)
- `Dockerfile` - Multi-stage build (Node frontend + Go backend)
- `helm/` - Helm chart for deploying the dashboard to Kubernetes
- `website/` - Landing page for claworc.com
- `website_docs/` - End-user documentation powered by Mintlify. It is automatically deployed to claworc.com/docs
- `docs/` - Detailed internal specs (architecture, API, data model, UI, features)

## Architecture
Expand All @@ -36,6 +36,16 @@ health at `/health`. Logs are streamed via SSE. WebSocket proxying for chat and
**LLM Gateway**: Proxy for LLM requests that replaces virtual keys with real, globally configured API tokens. It
records statistics in a separate SQLite database. See`docs/virtual-keys.md`.

**Agent Shim** (`internal/agentshim/`): The universal interface between the control plane and the AI agent
running inside an instance container (OpenClaw, Hermes, NanoClaw, custom). All agent-specific knowledge —
chat protocol, config paths, LLM provider config, restart — lives behind the `Client`/`Session` interfaces.
Two adapters: `shimexec/` speaks the exec-based shim contract (`docs/shim.md`, scripts at `/opt/claworc/shim/`
inside the image, invoked over SSH), and `openclawnative/` drives pre-shim OpenClaw images via their gateway
WebSocket + CLI. The factory prefers the shim when the image ships it and falls back to native for legacy
OpenClaw images. Chat, webhooks, config editing, and virtual-key routing all go through this layer. The
agent-type registry (`registry.go`) drives per-type defaults and UI capability gating. Layering is strict:
handlers → agentshim → sshproxy (transport) → orchestrator (containers).

**Orchestrator** (`internal/orchestrator/`): Thin abstraction over the underlying container runtime
(Kubernetes or Docker). Its job is generic container primitives only — instance lifecycle, exec, file
streaming, SSH address, resource updates, image updates, volume cloning. It does NOT own browser-pod,
Expand Down
35 changes: 28 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ include .env.development
export

AGENT_IMAGE := claworc/openclaw
HERMES_IMAGE := claworc/hermes
NANOCLAW_IMAGE := claworc/nanoclaw
STABLE_IMAGE := glukw/claworc-stable
STABLE_MIRROR_IMAGE := claworc/openclaw-stable
STABLE_VERSION_URL := https://isitstable.com/api/v1/openclaw/latest-stable
Expand Down Expand Up @@ -51,7 +53,9 @@ agent-base-china:

agent-build:
@echo "Building images locally (agent + browser variants)..."
docker buildx build --platform linux/$(NATIVE_ARCH) $(CACHE_ARGS) -t $(AGENT_IMAGE):$(TAG) -f agent/instance/Dockerfile --load agent/instance/
docker buildx build --platform linux/$(NATIVE_ARCH) $(CACHE_ARGS) -t $(AGENT_IMAGE):$(TAG) -f agent/openclaw/Dockerfile --load agent/openclaw/
docker buildx build --platform linux/$(NATIVE_ARCH) $(CACHE_ARGS) -t $(HERMES_IMAGE):$(TAG) -f agent/hermes/Dockerfile --load agent/hermes/
docker buildx build --platform linux/$(NATIVE_ARCH) $(CACHE_ARGS) -t $(NANOCLAW_IMAGE):$(TAG) -f agent/nanoclaw/Dockerfile --load agent/nanoclaw/
docker buildx build --platform linux/$(NATIVE_ARCH) $(CACHE_ARGS) --build-arg BASE_IMAGE=$(BROWSER_BASE_IMAGE):$(TAG) -t $(BROWSER_CHROMIUM_IMAGE):$(TAG) -f agent/browser/Dockerfile.chromium --load agent/browser/
docker buildx build --platform linux/amd64 $(CACHE_ARGS) --build-arg BASE_IMAGE=$(BROWSER_BASE_IMAGE):$(TAG) -t $(BROWSER_CHROME_IMAGE):$(TAG) -f agent/browser/Dockerfile.chrome --load agent/browser/
docker buildx build --platform linux/$(NATIVE_ARCH) $(CACHE_ARGS) --build-arg BASE_IMAGE=$(BROWSER_BASE_IMAGE):$(TAG) -t $(BROWSER_BRAVE_IMAGE):$(TAG) -f agent/browser/Dockerfile.brave --load agent/browser/
Expand All @@ -62,11 +66,28 @@ agent-test:
AGENT_CHROME_TEST_IMAGE=$(BROWSER_CHROME_IMAGE):$(TAG) \
AGENT_BRAVE_TEST_IMAGE=$(BROWSER_BRAVE_IMAGE):$(TAG) \
npm run test
@echo "Running shim conformance selftest against $(HERMES_IMAGE):$(TAG)..."
docker run --rm --entrypoint sh $(HERMES_IMAGE):$(TAG) -c 'sh /opt/claworc/shim/shim-selftest /opt/claworc/shim'
@echo "Running shim conformance selftest against $(NANOCLAW_IMAGE):$(TAG)..."
# NanoClaw's shim needs its s6 services (svc-agent supervisor) running, so
# boot the image, wait for health, then exec the selftest. The LLM proxy
# URL points at an unreachable port on purpose: the chat check then fails
# fast inside the agent (connection refused) instead of hanging on auth,
# and the turn still ends cleanly per contract.
docker rm -f claworc-nanoclaw-selftest >/dev/null 2>&1 || true
docker run -d --name claworc-nanoclaw-selftest \
-e CLAWORC_INITIAL_LLM_CONFIG='{"proxy_url":"http://127.0.0.1:40001","style":"anthropic","default_model":"anthropic/claude-sonnet-4-5","providers":[{"key":"anthropic","api_key":"claworc-vk-ci","api_type":"anthropic-messages"}]}' \
$(NANOCLAW_IMAGE):$(TAG)
sh -c 'for i in $$(seq 1 30); do docker exec claworc-nanoclaw-selftest /opt/claworc/shim/health >/dev/null 2>&1 && exit 0; sleep 2; done; echo "nanoclaw health never became ready" >&2; docker logs claworc-nanoclaw-selftest; docker rm -f claworc-nanoclaw-selftest; exit 1'
docker exec claworc-nanoclaw-selftest sh /opt/claworc/shim/shim-selftest /opt/claworc/shim; \
rc=$$?; docker rm -f claworc-nanoclaw-selftest >/dev/null 2>&1; exit $$rc


agent-push:
@echo "Pushing all agent + browser images in parallel..."
docker buildx build --platform $(PLATFORMS) $(CACHE_ARGS) -t $(AGENT_IMAGE):$(TAG) -f agent/instance/Dockerfile --push agent/instance/ & \
docker buildx build --platform $(PLATFORMS) $(CACHE_ARGS) -t $(AGENT_IMAGE):$(TAG) -f agent/openclaw/Dockerfile --push agent/openclaw/ & \
docker buildx build --platform $(PLATFORMS) $(CACHE_ARGS) -t $(HERMES_IMAGE):$(TAG) -f agent/hermes/Dockerfile --push agent/hermes/ & \
docker buildx build --platform $(PLATFORMS) $(CACHE_ARGS) -t $(NANOCLAW_IMAGE):$(TAG) -f agent/nanoclaw/Dockerfile --push agent/nanoclaw/ & \
docker buildx build --platform $(PLATFORMS) $(CACHE_ARGS) --build-arg BASE_IMAGE=$(BROWSER_BASE_IMAGE):$(TAG) -t $(BROWSER_CHROMIUM_IMAGE):$(TAG) -f agent/browser/Dockerfile.chromium --push agent/browser/ & \
docker buildx build --platform linux/amd64 $(CACHE_ARGS) --build-arg BASE_IMAGE=$(BROWSER_BASE_IMAGE):$(TAG) -t $(BROWSER_CHROME_IMAGE):$(TAG) -f agent/browser/Dockerfile.chrome --push agent/browser/ & \
docker buildx build --platform $(PLATFORMS) $(CACHE_ARGS) --build-arg BASE_IMAGE=$(BROWSER_BASE_IMAGE):$(TAG) -t $(BROWSER_BRAVE_IMAGE):$(TAG) -f agent/browser/Dockerfile.brave --push agent/browser/ & \
Expand All @@ -86,7 +107,7 @@ agent-stable:
-t $(STABLE_IMAGE):$(OPENCLAW_VERSION) \
-t $(STABLE_MIRROR_IMAGE):$(TAG) \
-t $(STABLE_MIRROR_IMAGE):$(OPENCLAW_VERSION) \
-f agent/instance/Dockerfile --push agent/instance/
-f agent/openclaw/Dockerfile --push agent/openclaw/

# CI variant: build single-arch first and run the OpenClaw test suite against
# the pinned image, only push multi-arch if tests pass.
Expand All @@ -97,7 +118,7 @@ agent-stable-ci:
@echo "Building+loading $(STABLE_IMAGE):test (openclaw@$(OPENCLAW_VERSION))..."
docker buildx build --platform linux/$(NATIVE_ARCH) $(CACHE_ARGS) \
--build-arg OPENCLAW_VERSION=$(OPENCLAW_VERSION) \
-t $(STABLE_IMAGE):test -f agent/instance/Dockerfile --load agent/instance/
-t $(STABLE_IMAGE):test -f agent/openclaw/Dockerfile --load agent/openclaw/
cd agent/tests && AGENT_INSTANCE_TEST_IMAGE=$(STABLE_IMAGE):test npm run test -- openclaw.test.ts
@echo "Pushing multi-arch $(STABLE_IMAGE) + $(STABLE_MIRROR_IMAGE) :$(TAG) and :$(OPENCLAW_VERSION)..."
docker buildx build --platform $(PLATFORMS) $(CACHE_ARGS) \
Expand All @@ -106,7 +127,7 @@ agent-stable-ci:
-t $(STABLE_IMAGE):$(OPENCLAW_VERSION) \
-t $(STABLE_MIRROR_IMAGE):$(TAG) \
-t $(STABLE_MIRROR_IMAGE):$(OPENCLAW_VERSION) \
-f agent/instance/Dockerfile --push agent/instance/
-f agent/openclaw/Dockerfile --push agent/openclaw/

AGENT_CONTAINER := claworc-agent-exec
AGENT_SSH_PORT := 2222
Expand Down Expand Up @@ -180,11 +201,11 @@ dev:
CLAWORC_AUTH_DISABLED=true CLAWORC_LLM_RESPONSE_LOG=$(CURDIR)/llm-responses.log CLAWORC_ALLOWED_HOST_MOUNTS=/tmp,~/ goreman -set-ports=false start

ssh-integration-test:
docker build -f agent/instance/Dockerfile -t claworc-agent:local agent/instance/
docker build -f agent/openclaw/Dockerfile -t claworc-agent:local agent/openclaw/
cd control-plane && go test -tags docker_integration -v -timeout 300s ./internal/sshproxy/ -run TestIntegration

ssh-file-integration-test:
docker build -f agent/instance/Dockerfile -t claworc-agent:local agent/instance/
docker build -f agent/openclaw/Dockerfile -t claworc-agent:local agent/openclaw/
cd agent/tests && npm run test:ssh -- --testPathPattern file.test

test-integration-backend:
Expand Down
2 changes: 1 addition & 1 deletion agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ All services are managed by s6-overlay:
| Service | Port | Description |
|----------------|-------|--------------------------------|
| sshd | 22 | SSH server for remote access |
| svc-openclaw | 18789 | OpenClaw gateway |
| svc-agent | 18789 | OpenClaw gateway |
| svc-xvnc | 5900 | TigerVNC X server |
| svc-novnc | 3000 | noVNC websockify bridge |
| svc-desktop | - | Openbox + Chromium |
Expand Down
File renamed without changes.
113 changes: 113 additions & 0 deletions agent/hermes/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Claworc Hermes agent image (claworc/hermes).
#
# Hermes Agent by Nous Research (https://github.com/NousResearch/hermes-agent)
# behind the Claworc Agent Shim Contract v1 (docs/shim.md): debian-slim,
# s6-overlay as PID 1, the hardened sshd, and a shim at /opt/claworc/shim
# that drives Hermes' non-interactive CLI (`hermes chat -q ... -Q`) with
# native resume-by-session-id persistence.
#
# Lean by design: no VNC/browser (those live in the claworc/<browser>-browser
# images) and no Hermes messaging-gateway daemon — each chat turn spawns the
# CLI, so there is no svc-agent service and the shim's restart verb is a
# contract-legal no-op.

FROM debian:bookworm-slim

ARG S6_OVERLAY_VERSION=3.2.0.2
ARG TARGETARCH
# Pinned Hermes release tag (or any git ref) from NousResearch/hermes-agent.
ARG HERMES_VERSION=v2026.8.3

# Create claworc user (UID 1000) — all agent state stays owned by it.
RUN useradd -m -u 1000 -s /bin/bash claworc

# System packages: sshd (the contract's only hard runtime dependency),
# python3 + venv for Hermes (bookworm ships 3.11, inside Hermes'
# requires-python >=3.11,<3.14), git (Hermes install + its git/worktree
# tooling), ripgrep (Hermes' file-search tools), jq/procps for the shim
# and selftest.
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates curl xz-utils \
openssh-server \
python3 python3-venv \
git ripgrep jq procps && \
rm -rf /var/lib/apt/lists/*

RUN S6_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64") && \
curl -fsSL "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" \
| tar -C / -Jxpf - && \
curl -fsSL "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" \
| tar -C / -Jxpf -

# uv — the package manager Hermes' own installer uses. Multi-arch static
# binaries copied from the pinned upstream image.
COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /usr/local/bin/

# ---------------------------------------------------------------------------
# Hermes install, pinned to HERMES_VERSION.
#
# Mirrors the official managed layout (full checkout + editable install +
# venv OUTSIDE the source tree — see the hermes-agent README's contributor
# notes): repo-relative data (locales/, skills/, prompts) resolves from the
# checkout, and the venv can't be clobbered by agent file operations.
# Base dependencies only (exact-pinned upstream); the heavy `[all]` extra
# (voice, messaging platforms) is deliberately skipped — Hermes lazy-installs
# optional backends via uv at runtime when a user enables them.
# ---------------------------------------------------------------------------
RUN git clone --depth 1 --branch "${HERMES_VERSION}" \
https://github.com/NousResearch/hermes-agent /opt/hermes/hermes-agent && \
uv venv --python /usr/bin/python3 /opt/hermes/venv && \
VIRTUAL_ENV=/opt/hermes/venv uv pip install -e /opt/hermes/hermes-agent && \
ln -s /opt/hermes/venv/bin/hermes /usr/local/bin/hermes && \
printf '%s\n' "${HERMES_VERSION}" > /opt/hermes/VERSION && \
/opt/hermes/venv/bin/python -m compileall -q /opt/hermes/hermes-agent || true

# Sanity check: the pinned install must answer --version (the shim's health
# verb runs the same probe at runtime).
RUN /opt/hermes/venv/bin/hermes --version

# Baked ~/.hermes skeleton: minimal config.yaml (with the claworc-managed
# model block configure-llm rewrites) + empty .env. Hermes needs no
# interactive onboarding when model.provider/base_url/api_key are present in
# config.yaml (hermes_cli/main.py:_has_any_provider_configured). Seeded onto
# the PVC by the init-agent-seed oneshot (or lazily by the shim verbs).
COPY skeleton/ /opt/hermes-skeleton/.hermes/
RUN chown -R claworc:claworc /opt/hermes-skeleton && \
chmod 0644 /opt/hermes-skeleton/.hermes/config.yaml /opt/hermes-skeleton/.hermes/.env

COPY rootfs/ /

# Claworc agent shim (docs/shim.md): the universal exec-based interface the
# control plane invokes over SSH. Verbs must be 0755; agent.txt/agent.svg are
# static identity files read over SFTP.
COPY shim/ /opt/claworc/shim/
RUN chmod 0755 /opt/claworc/shim/meta \
/opt/claworc/shim/health \
/opt/claworc/shim/chat-send \
/opt/claworc/shim/chat-abort \
/opt/claworc/shim/session-reset \
/opt/claworc/shim/config-get \
/opt/claworc/shim/config-set \
/opt/claworc/shim/configure-llm \
/opt/claworc/shim/restart \
/opt/claworc/shim/shim-selftest \
/opt/claworc/shim/lib/ensure-seed.sh && \
chmod 0644 /opt/claworc/shim/agent.txt /opt/claworc/shim/agent.svg

RUN chmod +x /etc/s6-overlay/s6-rc.d/init-setup/up \
/etc/s6-overlay/s6-rc.d/init-agent-seed/up \
/etc/s6-overlay/scripts/init-setup.sh \
/etc/s6-overlay/scripts/init-agent-seed.sh \
/etc/s6-overlay/s6-rc.d/svc-sshd/run

# Pre-create the agent log dir (init-setup.sh recreates it at boot anyway).
RUN mkdir -p /var/log/claworc && chown claworc:claworc /var/log/claworc

# Reduce SUID surface. Stripping su's SUID bit is safe: the shim invokes it
# as root (SSH exec), and root does not need SUID to switch users.
RUN chmod u-s /usr/bin/su /usr/bin/mount /usr/bin/umount /usr/bin/newgrp \
/usr/bin/chsh /usr/bin/chfn /usr/bin/gpasswd /usr/bin/chage \
/usr/lib/openssh/ssh-keysign 2>/dev/null || true

ENTRYPOINT ["/init"]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/command/with-contenv /etc/s6-overlay/scripts/init-agent-seed.sh
File renamed without changes.
28 changes: 28 additions & 0 deletions agent/hermes/rootfs/etc/s6-overlay/scripts/init-agent-seed.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash
# Seeds /home/claworc/.hermes from the image-baked skeleton on first boot,
# then applies the initial LLM routing passed by the control plane.
#
# The Dockerfile bakes a minimal ~/.hermes tree (config.yaml with the
# claworc-managed model block + an empty .env) into /opt/hermes-skeleton.
# This oneshot copies it onto the (possibly-empty) PVC mounted at
# /home/claworc. Idempotent: a no-op when /home/claworc/.hermes already
# exists (PVC carried state from a previous boot).
#
# The heavy lifting is shared with the shim verbs via ensure-seed.sh so the
# selftest / verbs also work in containers that never ran the s6 boot
# sequence (e.g. `docker run --entrypoint sh`).

set -e

/opt/claworc/shim/lib/ensure-seed.sh

# ---------------------------------------------------------------------------
# First-boot LLM routing: the control plane passes the configure-llm routing
# document in CLAWORC_INITIAL_LLM_CONFIG (docs/shim.md). Apply it through the
# shim's own verb so boot and reconfiguration share one code path.
# ---------------------------------------------------------------------------
if [ -n "${CLAWORC_INITIAL_LLM_CONFIG:-}" ]; then
if ! printf '%s' "$CLAWORC_INITIAL_LLM_CONFIG" | /opt/claworc/shim/configure-llm; then
echo "configure-llm failed; continuing boot without initial LLM routing" >&2
fi
fi
Loading
Loading