diff --git a/.dockerignore b/.dockerignore index 75a34e9a4b..319e3044f1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,4 @@ node_modules ops scripts/* !scripts/check-node-version.mjs +!scripts/openclaw-cli-shim.py diff --git a/.env.example b/.env.example index 77b1e95495..a7e80ca598 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,6 @@ # ═══════════════════════════════════════════════════════════════════════════════ # Mission Control — Environment Variables # ═══════════════════════════════════════════════════════════════════════════════ -# Copy to .env and adjust for your deployment mode. # # IMPORTANT: NEXT_PUBLIC_* variables are baked into the client-side JavaScript # bundle at build time (pnpm build). If you change any NEXT_PUBLIC_* variable, @@ -9,9 +8,25 @@ # Server-side variables (OPENCLAW_*, AUTH_*, etc.) are read at runtime and do # not require a rebuild. +# MC_MODE=prod +MC_MODE=dev +# 0, если нужно без OpenClaw +OPENCLAW_ENABLED=1 + # === Server Port === # PORT=3000 +# === Make / Docker local runtime === +# `make up` and `make status` resolve MC_URL from these keys. + +# --- Mission Control: базовый URL приложения --- +# Протокол внешнего доступа к MC: http для локальной разработки, https для прод/публичного доступа. +MC_URL_SCHEME=http +# Хост, на котором доступен Mission Control. +MC_HOST=127.0.0.1 +# Порт Mission Control на хосте. +MC_PORT=7012 + # ═══════════════════════════════════════════════════════════════════════════════ # Authentication # ═══════════════════════════════════════════════════════════════════════════════ @@ -28,13 +43,22 @@ # Auto-generated on first run if not set. Persisted to .data/.auto-generated. # AUTH_SECRET= -MC_COOKIE_SECURE= -MC_COOKIE_SAMESITE=strict - +# --- Опционально: безопасность Mission Control --- # Network access control (production: blocked unless host is explicitly allowed) # Patterns: exact "app.example.com", subdomain "*.example.com", prefix "100.*" -# MC_ALLOW_ANY_HOST= +# Разрешённые host-заголовки через запятую. MC_ALLOWED_HOSTS=localhost,127.0.0.1,::1 +# Включить HSTS (ТОЛЬКО при HTTPS, иначе можно сломать доступ по HTTP). +MC_ENABLE_HSTS= +# Secure-cookie (ТОЛЬКО при HTTPS; для HTTP оставьте пустым). +# Set to 1 only when Mission Control is served over HTTPS. +# Keep blank for local/plain-HTTP deployments. +MC_COOKIE_SECURE= + +MC_COOKIE_SAMESITE=strict +# Set to 1 only when TLS terminates at your proxy/load-balancer. +# MC_ENABLE_HSTS=1 + # Trusted reverse proxy / header authentication # MC_PROXY_AUTH_HEADER=X-User-Email @@ -57,11 +81,33 @@ NEXT_PUBLIC_GOOGLE_CLIENT_ID= # For local development, both point to the same machine (127.0.0.1). # For Docker or remote servers, they will differ. -# ─── Local Development ──────────────────────────────────────────────────────── -# Both MC and gateway run on the same machine. No special config needed. -OPENCLAW_GATEWAY_HOST=127.0.0.1 -OPENCLAW_GATEWAY_PORT=18789 -# OPENCLAW_GATEWAY_TOKEN= +## ─── Local Development ──────────────────────────────────────────────────────── +## Both MC and gateway run on the same machine. No special config needed. +#OPENCLAW_GATEWAY_HOST=127.0.0.1 +# +## --- OpenClaw: хост для health/status проверок --- +#OPENCLAW_STATUS_HOST=127.0.0.1 +# +## --- OpenClaw: внешние порты (host bind) --- +## Внешний порт OpenClaw gateway. +#OPENCLAW_GATEWAY_PORT=18789 +## Внешний порт панели управления OpenClaw (Control UI). +#OPENCLAW_CONTROL_UI_PORT=18791 +# +## --- OpenClaw: внутренние/bridge порты --- +## Внутренний порт gateway внутри контейнера. +#OPENCLAW_GATEWAY_INTERNAL_PORT=18789 +## Внешний порт bridge на хосте. +#OPENCLAW_BRIDGE_PORT=18790 +## Внутренний порт bridge внутри контейнера. +#OPENCLAW_BRIDGE_INTERNAL_PORT=18790 + +# --- OpenClaw: токен доступа к gateway (секрет) --- +# Секретный токен для авторизации в OpenClaw gateway. +# Никогда не публикуйте реальный токен в git. +OPENCLAW_GATEWAY_TOKEN=... + + NEXT_PUBLIC_GATEWAY_HOST= NEXT_PUBLIC_GATEWAY_PORT=18789 @@ -105,6 +151,12 @@ NEXT_PUBLIC_GATEWAY_PORT=18789 OPENCLAW_HOME= # OPENCLAW_CONFIG_PATH= OPENCLAW_TOOLS_PROFILE=coding +# Visibility of group chat replies when projecting OpenClaw state (automatic|message_tool). +OPENCLAW_MESSAGES_GROUPCHAT_VISIBLE_REPLIES=automatic +# Sandbox tooling relies on Docker; the OpenClaw stack mounts /var/run/docker.sock read/write when enabled. +# Optional: if the socket gid differs from the container's Docker group, set DOCKER_SOCKET_GID. +# When unset, the compose stack auto-detects from the mounted socket. +DOCKER_SOCKET_GID= NEXT_PUBLIC_GATEWAY_PROTOCOL= NEXT_PUBLIC_GATEWAY_URL= @@ -120,6 +172,14 @@ MC_DEFAULT_GATEWAY_NAME=primary MC_COORDINATOR_AGENT=coordinator NEXT_PUBLIC_COORDINATOR_AGENT=coordinator +# ═══════════════════════════════════════════════════════════════════════════════ +# Automation (optional) +# ═══════════════════════════════════════════════════════════════════════════════ +# Enable automatic daily backups without toggling it in the UI. Accepts 1/true/yes/on. +# Backup directory will be created automatically when scheduled backups run. +# Example: MC_AUTO_BACKUP=1 +MC_AUTO_BACKUP= + # ═══════════════════════════════════════════════════════════════════════════════ # Data Paths (all optional, defaults to .data/ in project root) # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/.gitignore b/.gitignore index 30e8f42073..30e8a42544 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,29 @@ playwright-report/ # Claude Code context files (root CLAUDE.md is committed for AI agent discovery) **/CLAUDE.md !/CLAUDE.md + +# Local agent walkthrough (kept on disk, not committed) +WALKTHROUGH.md + +# IDE files +/.idea/ +/.vscode/ + +# Third-party service clones — naming pattern: -src/ +# Each is an independent repo; we only check in the compose service that +# references it, never the upstream code. +/openclaw-src/ +/gpu-coordinator-proxy-src/ + +# Persistent state + secrets, also local-only. +/.openclaw-data/ +/.mc-openclaw/ +/.env.openclaw +/examples/ + +# Dolt database files (added by bd init) +.dolt/ +*.db +/.beads/ +/.beads/ +/.vibe/ diff --git a/Dockerfile b/Dockerfile index cb69aa1fc6..39da39f892 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ FROM base AS deps COPY package.json ./ COPY pnpm-lock.yaml* ./ # better-sqlite3 requires native compilation tools -RUN apt-get update && apt-get install -y python3 make g++ --no-install-recommends && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get upgrade && apt-get install -y python3 make g++ --no-install-recommends && rm -rf /var/lib/apt/lists/* RUN if [ -f pnpm-lock.yaml ]; then \ pnpm install --frozen-lockfile; \ else \ @@ -32,8 +32,20 @@ WORKDIR /app ENV NODE_ENV=production # curl, CA certs, python3, git needed for agent runtime installers (OpenClaw, Hermes) # procps provides `ps` and `uptime` used by system-monitor APIs -RUN apt-get update && apt-get install -y curl ca-certificates python3 git make g++ procps --no-install-recommends && rm -rf /var/lib/apt/lists/* -RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +RUN apt-get update && apt-get upgrade && apt-get install -y curl ca-certificates python3 git make g++ procps tmux jq --no-install-recommends && rm -rf /var/lib/apt/lists/* + +# Bake Claude Code + Codex CLIs into the image as a fallback so the +# Settings → Agent Runtimes panel reports "Installed" even before the host +# bind-mounts (compose adds ${HOME}/.local/bin at runtime, which takes +# precedence in PATH and provides authenticated host binaries). +RUN npm install -g @anthropic-ai/claude-code @openai/codex 2>&1 | tail -5 + +# node:22-slim already ships a `node` user at uid 1000; reuse it as our +# `nextjs` alias so bind-mounted host files (typical Linux uid 1000) read directly. +RUN if ! id -u nextjs >/dev/null 2>&1; then \ + usermod --login nextjs --move-home --home /home/nextjs node && \ + groupmod --new-name nodejs node ; \ + fi COPY --from=build /app/.next/standalone ./ COPY --from=build /app/.next/static ./.next/static COPY --from=build /app/public ./public @@ -46,7 +58,8 @@ RUN mkdir -p .data && chown nextjs:nodejs .data RUN echo 'const http=require("http");const r=http.get("http://localhost:"+(process.env.PORT||3000)+"/api/status?action=health",s=>{process.exit(s.statusCode===200?0:1)});r.on("error",()=>process.exit(1));r.setTimeout(4000,()=>{r.destroy();process.exit(1)})' > /app/healthcheck.js COPY docker-entrypoint.sh /app/docker-entrypoint.sh RUN chmod 755 /app/docker-entrypoint.sh && \ - chmod -R a+rX /app/public/ /app/src/ + chmod -R a+rX /app/public/ /app/src/ && \ + chown -R nextjs:nodejs /app /home/nextjs USER nextjs ENV PORT=3000 EXPOSE 3000 diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000000..f240c7b9cf --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,81 @@ +# Dockerfile.dev — image for `make dev` (hot-reload, no production build). +# +# Source code is bind-mounted from the host at runtime; this image only +# bakes the OS deps, pnpm, node_modules, and the host-fallback CLIs. +# Rebuild only when the dependency manifest or system tooling changes. +FROM node:22.22.0-slim +RUN corepack enable && corepack prepare pnpm@latest --activate +WORKDIR /app + +# OS deps: better-sqlite3 build tools, agent-runtime probes, /chat PTY, +# docker CLI (talks to host daemon via bind-mounted /var/run/docker.sock — +# required so `openclaw doctor` can verify sandbox readiness without +# emitting the "Docker is not available" warning to the Doctor panel). +RUN apt-get update && apt-get upgrade && apt-get install -y \ + curl ca-certificates python3 git make g++ procps tmux jq docker.io \ + --no-install-recommends && rm -rf /var/lib/apt/lists/* + +# Bake claude / codex CLIs as a fallback. The host's ~/.local/bin still wins +# at runtime via PATH order, so an authenticated host install transparently +# shadows these baked copies. +RUN npm install -g @anthropic-ai/claude-code @openai/codex 2>&1 | tail -5 + +# OpenClaw CLI is provided as a bind-mounted shim, NOT a baked npm install. +# `docker-compose-dev.yml` mounts the cloned openclaw-src/ at /opt/openclaw-src, +# and this shim runs `node /opt/openclaw-src/dist/index.js "$@"`. To update +# openclaw, run `make openclaw-update` on the host — it git-pulls openclaw-src +# and rebuilds dist via the openclaw-builder container; both this shim AND the +# gateway daemon pick up the new dist on the next call (no docker rebuild). +# +# The shim ALSO transparently rewrites legacy CLI shapes that MC source uses +# but that openclaw 2026.4.x has retired. Specifically MC's wake / agent +# message endpoints call `openclaw gateway sessions_send --session X --message +# Y`, but the daemon now exposes that only as the RPC method `chat.send` +# behind `openclaw gateway call chat.send --params {...}`. Rewriting in the +# shim keeps MC unmodified while staying compatible with new openclaw. +# +# The shim itself is bind-mounted from ./scripts/openclaw-cli-shim.py via +# docker-compose-dev.yml so edits land live without rebuilding this image. +RUN cat > /usr/local/bin/openclaw <<'SHIM' && chmod +x /usr/local/bin/openclaw +#!/bin/sh +# Live-loaded openclaw CLI shim. Source: /opt/openclaw-src (bind-mounted from +# host's openclaw-src/ clone). Updates via `make openclaw-update`. +if [ ! -f /opt/openclaw-src/dist/index.js ]; then + echo "openclaw shim: /opt/openclaw-src/dist/index.js not found." >&2 + echo " Did you run 'make openclaw-build' to populate openclaw-src/dist/?" >&2 + exit 127 +fi +if [ -f /usr/local/lib/openclaw-cli-shim.py ]; then + exec python3 /usr/local/lib/openclaw-cli-shim.py "$@" +fi +exec node /opt/openclaw-src/dist/index.js "$@" +SHIM + +# Install deps from manifests only — never from source. The source tree is +# bind-mounted at runtime, so changes to .ts/.tsx/.css don't invalidate this +# layer. Only changes to package.json / pnpm-lock.yaml trigger a rebuild. +COPY package.json pnpm-lock.yaml* ./ +RUN if [ -f pnpm-lock.yaml ]; then \ + pnpm install --frozen-lockfile; \ + else \ + echo "WARN: pnpm-lock.yaml not found in build context; running non-frozen install" && \ + pnpm install --no-frozen-lockfile; \ + fi + +# Reuse uid 1000 (the slim image's `node` user) as `nextjs`, matching prod +# image exactly so bind-mounted host files keep their ownership. +RUN if ! id -u nextjs >/dev/null 2>&1; then \ + usermod --login nextjs --move-home --home /home/nextjs node && \ + groupmod --new-name nodejs node ; \ + fi +RUN mkdir -p .data .next && chown -R nextjs:nodejs /app /home/nextjs + +USER nextjs +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 +ENV NODE_ENV=development +EXPOSE 3000 + +# Default command — overridden by docker-compose-dev.yml. Provided here so +# the image is also runnable standalone (`docker run mission-control-dev`). +CMD ["pnpm", "exec", "next", "dev", "--hostname", "0.0.0.0", "--port", "3000"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..25d82af687 --- /dev/null +++ b/Makefile @@ -0,0 +1,124 @@ +# Minimal docker-compose wrapper. +# +# Usage: +# make # rebuild + (re)start everything, prune orphans +# make build [SERVICE...] # build images (all, or specific services) +# make up [SERVICE...] # start services +# make down # stop all +# make restart [SERVICE...] # restart +# make logs [SERVICE...] # follow logs (Ctrl+C to stop) +# make ps # list running services +# make clean # down + prune orphan containers, dangling images, unused volumes +# +# Mode: +# MODE=dev (default) — uses docker-compose-dev.yml + docker-compose-openclaw.yml +# MODE=prod — uses docker-compose.yml + docker-compose-openclaw.yml +# +# make MODE=prod up +# +# Service args are positional and forwarded to docker compose: +# make logs openclaw-gateway +# make build mission-control-dev gpu-coordinator-proxy +# make restart ollama +# +# Old recipes (openclaw-pair-mc, openclaw-update, etc.) live in Makefile.legacy. +# Include them only if you actually need them: `make -f Makefile.legacy `. + +MODE ?= dev + +ifeq ($(MODE),prod) + COMPOSE_FILES := -f docker-compose.yml -f docker-compose-openclaw.yml +else + COMPOSE_FILES := -f docker-compose-dev.yml -f docker-compose-openclaw.yml +endif + +# Auto-detect the host's docker socket gid so the dev container's group_add +# matches whatever the host actually has (994 on stock Debian/Ubuntu, but +# varies on Fedora/Arch/colima/Rancher Desktop). Override on the command +# line: `DOCKER_SOCKET_GID=999 make up`. Falls back to 994 if the socket +# isn't readable. +DOCKER_SOCKET_GID ?= $(shell stat -c %g /var/run/docker.sock 2>/dev/null || echo 994) +export DOCKER_SOCKET_GID + +DC := docker compose $(COMPOSE_FILES) + +# Everything after the first goal is treated as service args, not as targets. +# `make build openclaw-gateway` → first goal `build`, args=`openclaw-gateway`. +ARGS := $(filter-out $(firstword $(MAKECMDGOALS)),$(MAKECMDGOALS)) + +.DEFAULT_GOAL := all + +.PHONY: all build up down restart logs ps clean help build-extra-images + +help: ## Show this help + @awk 'BEGIN {FS = ":.*?## "} /^[a-z][a-zA-Z0-9_-]*:.*## / {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @echo "" + @echo " Mode: MODE=$(MODE) (override with MODE=prod)" + @echo " Compose files: $(COMPOSE_FILES)" + +all: ## Default: prune orphans, rebuild images, recreate everything, show ps + @echo "==> [1/4] stopping running stack and pruning orphans" + -$(DC) down --remove-orphans + @echo "" + @echo "==> [2/4] building images" + $(DC) build --pull $(ARGS) + @$(MAKE) -s build-extra-images + @echo "" + @echo "==> [3/4] starting services" + $(DC) up -d --remove-orphans $(ARGS) + @echo "" + @echo "==> [4/4] status" + $(DC) ps + +build: ## Build images. Usage: make build [SERVICE...] + $(DC) build --pull $(ARGS) + @$(MAKE) -s build-extra-images + +# Standalone images that are NOT in any compose file but are needed at runtime +# (e.g. the sandbox image that openclaw spawns from .openclaw-data/openclaw.json +# settings, not from a compose service). Bootstraps the upstream +# openclaw-sandbox:bookworm-slim base via openclaw-src/scripts/sandbox-setup.sh +# if missing, then overlays brew via Dockerfile.openclaw.sandbox. +build-extra-images: + @if ! docker image inspect openclaw-sandbox:bookworm-slim >/dev/null 2>&1; then \ + if [ -x openclaw-src/scripts/sandbox-setup.sh ]; then \ + echo "==> bootstrapping openclaw-sandbox:bookworm-slim (upstream)"; \ + bash openclaw-src/scripts/sandbox-setup.sh; \ + else \ + echo "WARN: openclaw-src/scripts/sandbox-setup.sh missing; skipping sandbox base"; \ + fi; \ + fi + @if [ -f Dockerfile.openclaw.sandbox ]; then \ + echo "==> building mc-openclaw-sandbox:brew (overlays brew on the upstream sandbox)"; \ + docker build -t mc-openclaw-sandbox:brew -f Dockerfile.openclaw.sandbox .; \ + fi + @if [ -f Dockerfile.openclaw.dockercli ]; then \ + echo "==> building mc-openclaw:dockercli (gateway image with docker CLI + brew)"; \ + docker build -t mc-openclaw:dockercli -f Dockerfile.openclaw.dockercli .; \ + fi + +up: ## Start services. Usage: make up [SERVICE...] + $(DC) up -d --remove-orphans $(ARGS) + +down: ## Stop and remove all services + $(DC) down --remove-orphans + +restart: ## Restart services. Usage: make restart [SERVICE...] + $(DC) restart $(ARGS) + +logs: ## Follow logs. Usage: make logs [SERVICE...] + $(DC) logs -f --tail=100 $(ARGS) + +ps: ## Show running services + $(DC) ps + +clean: ## Down + prune dangling images, unused volumes, leftover containers + -$(DC) down --remove-orphans --volumes + -docker container prune -f + -docker image prune -f + -docker volume prune -f + +# Catch-all so positional args like `make logs openclaw-gateway` don't +# print "No rule to make target 'openclaw-gateway'". +%: + @: diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000000..02d000f5ca --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,192 @@ +# Dev compose — bind-mounts the source tree so `pnpm dev` hot-reloads +# without rebuilding the image. Use via `make dev`. +# +# When to rebuild the image (`make dev-build`): +# - package.json / pnpm-lock.yaml changed +# - Dockerfile.dev changed (added system tool, OS dep, etc.) +# +# Day-to-day code edits in src/, public/, messages/ — just hit save, the +# next.js dev server picks them up. +services: + mission-control-dev: + build: + context: . + dockerfile: Dockerfile.dev + image: mission-control-dev:latest + container_name: mission-control-dev + ports: + - "${MC_PORT:-7012}:${PORT:-3000}" + environment: + - NODE_ENV=development + - PORT=${PORT:-3000} + - PATH=/home/nextjs/.local/bin:/home/nextjs/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - HOME=/home/nextjs + - OPENCLAW_GATEWAY_HOST=${OPENCLAW_GATEWAY_HOST:-host.docker.internal} + - OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT:-18789} + # Gateway URL + token are read by the openclaw CLI (baked into this + # image) when it talks to the gateway daemon over WebSocket. + - OPENCLAW_GATEWAY_URL=${OPENCLAW_GATEWAY_URL:-ws://host.docker.internal:18789} + - OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN:-} + - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} + - TELEGRAM_NUMERIC_USER_ID=${TELEGRAM_NUMERIC_USER_ID:-} + - TELEGRAM_DM_POLICY=${TELEGRAM_DM_POLICY:-} + - TELEGRAM_ALLOW_FROM=${TELEGRAM_ALLOW_FROM:-} + - TELEGRAM_OWNER_ALLOW_FROM=${TELEGRAM_OWNER_ALLOW_FROM:-} + - OPENCLAW_TOOLS_PROFILE=${OPENCLAW_TOOLS_PROFILE:-coding} + - OPENCLAW_SECURITY_WORKSPACE_ONLY=${OPENCLAW_SECURITY_WORKSPACE_ONLY:-1} + - OPENCLAW_SECURITY_DENY_AUTOMATION=${OPENCLAW_SECURITY_DENY_AUTOMATION:-1} + - OPENCLAW_SECURITY_DENY_RUNTIME=${OPENCLAW_SECURITY_DENY_RUNTIME:-1} + - OPENCLAW_SECURITY_DENY_FS=${OPENCLAW_SECURITY_DENY_FS:-0} + - OPENCLAW_SECURITY_SANDBOX_ALL=${OPENCLAW_SECURITY_SANDBOX_ALL:-1} + - OPENCLAW_MESSAGES_GROUPCHAT_VISIBLE_REPLIES=${OPENCLAW_MESSAGES_GROUPCHAT_VISIBLE_REPLIES:-automatic} + # Break-glass for plaintext ws:// to host.docker.internal — the docker + # bridge network is private to this host. Production deployments should + # use wss:// or an SSH tunnel instead. + - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-1} + - MC_ALLOWED_HOSTS=${MC_ALLOWED_HOSTS:-localhost,127.0.0.1,::1} + # Browser-facing gateway URL. The MC backend (inside container) reaches + # the gateway at host.docker.internal:18789, but a browser on the host + # cannot resolve that name and must use the published port directly. + # `NEXT_PUBLIC_GATEWAY_URL` overrides what the WebSocket client connects + # to (see src/app/api/gateways/connect/route.ts:156). + - NEXT_PUBLIC_GATEWAY_URL=${NEXT_PUBLIC_GATEWAY_URL:-ws://127.0.0.1:18789} + # Gateway optional from the browser's perspective. Even with the URL + # right, a browser WS connection comes in from the docker bridge IP, + # not loopback — openclaw treats that as an unpaired device and closes + # the WebSocket with code 1006. Each browser session would need its own + # pairing approval flow, which is impractical for a dev-stack UI. With + # GATEWAY_OPTIONAL=true the MC frontend stops retrying WS forever and + # falls back to HTTP polling for live updates (status, sessions, etc.). + # Backend still uses the gateway for task dispatch, so MC functionality + # is unaffected — only the WebSocket-driven live event stream is off. + - NEXT_PUBLIC_GATEWAY_OPTIONAL=${NEXT_PUBLIC_GATEWAY_OPTIONAL:-true} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + - LOCAL_LLM_ENDPOINT=${LOCAL_LLM_ENDPOINT:-http://host.docker.internal:1234/v1} + - LOCAL_LLM_API_KEY=${LOCAL_LLM_API_KEY:-} + - MC_HOST_SESSION_MODE=${MC_HOST_SESSION_MODE:-coexist} + - NEXT_PUBLIC_CHAT_POLL_INTERVAL_MS=${NEXT_PUBLIC_CHAT_POLL_INTERVAL_MS:-1000} + env_file: + - path: .env + required: false + - path: .env.openclaw + required: false + user: "1000:1000" + # Make the host's docker socket group accessible to uid 1000 so + # `openclaw doctor`, called from src/app/api/openclaw/doctor/route.ts via + # runOpenClaw, can run `docker version` and stop emitting the + # "Sandbox mode is enabled but Docker is not available" warning. + # The Makefile auto-detects the gid via `stat -c %g /var/run/docker.sock` + # and exports DOCKER_SOCKET_GID; the 994 fallback matches a stock + # `docker` group on Debian/Ubuntu hosts when invoked outside `make`. + group_add: + - "${DOCKER_SOCKET_GID:-994}" + volumes: + # Host docker socket — needed for `openclaw doctor` sandbox readiness + # check inside the MC container. The socket is bind-mounted; uid 1000 + # gains access via `group_add` above. + - /var/run/docker.sock:/var/run/docker.sock:rw + # ── Source code: bind-mount, hot-reload ── + - ./src:/app/src:rw + - ./public:/app/public:rw + - ./messages:/app/messages:rw + - ./scripts:/app/scripts:ro + - ./next.config.js:/app/next.config.js:ro + - ./tailwind.config.js:/app/tailwind.config.js:ro + - ./postcss.config.js:/app/postcss.config.js:ro + - ./tsconfig.json:/app/tsconfig.json:ro + - ./eslint.config.mjs:/app/eslint.config.mjs:ro + # package.json + lock — pnpm checks them at startup; bind-mount so + # adding a dep with `pnpm add` on the host is reflected on next restart + # (still requires `make dev-build` if native modules change). + - ./package.json:/app/package.json:ro + - ./pnpm-lock.yaml:/app/pnpm-lock.yaml:ro + + # ── Persistent state ── + # Share the production data volume so admin user, workspaces, projects, + # and agents created via `make up` are visible in `make dev` and vice + # versa. Stop one before starting the other (SQLite single-writer). + - mission-control_mc-data:/app/.data + # .next dev cache — keep across restarts so first paint is fast. + - mc-next-dev:/app/.next + + # ── Host configs (same projection as production compose) ── + - ${HOME}/.local/bin:/home/nextjs/.local/bin:rw + - ${HOME}/.local/share/claude:/home/nextjs/.local/share/claude:rw + - ${HOME}/.bun:/home/nextjs/.bun:rw + - ${HOME}/.claude:/home/nextjs/.claude:rw + - ${HOME}/.claude.json:/home/nextjs/.claude.json:rw + - /mnt:/mnt:rw + - ${HOME}:${HOME}:rw + # Mask the host's `${HOME}/.openclaw` so openclaw doctor inside the + # container only sees one state directory (`/home/nextjs/.openclaw`, + # mounted from ./.mc-openclaw below). Without this mask, openclaw + # scans `/home/*/.openclaw` (doctor-state-integrity.ts:findOtherStateDirs) + # and finds the host user's `~/.openclaw` through the `${HOME}:${HOME}` + # bind above, then emits "Multiple state directories detected" — which + # raises the MC doctor banner (level=warning) even when the only finding + # is the bind-mount artifact. + # + # We bind-mount a regular empty FILE from the project — `existsDir()` + # in openclaw uses `statSync(path).isDirectory()`, so a non-directory + # at that path makes openclaw skip it entirely. Tmpfs doesn't work + # here (it would still appear as a directory). + # + # Prerequisite: the host's `${HOME}/.openclaw` must NOT exist as a + # directory before container start, otherwise docker rejects the + # bind-mount with "Are you trying to mount a directory onto a file". + # Run `rm -rf "$HOME/.openclaw"` once on the host; nothing inside + # the container creates it back automatically. + - ./.docker-mask/openclaw-stub.empty:${HOME}/.openclaw:ro + # ── OpenClaw CLI state (persists pairing identity + device-auth.json + # so MC keeps the same paired identity across container restarts). + # Bound to ./.mc-openclaw/ on the host for direct filesystem patching + # by `make openclaw-pair-mc`. ── + - ./.mc-openclaw:/home/nextjs/.openclaw:rw + # ── OpenClaw CLI runtime (live-updateable via `make openclaw-update`). + # The /usr/local/bin/openclaw shim baked into Dockerfile.dev runs + # `node /opt/openclaw-src/dist/index.js`, so updating dist on the host + # immediately changes the CLI's behaviour with no image rebuild. ── + - ./openclaw-src:/opt/openclaw-src:ro + # ── Compat shim: rewrites retired openclaw CLI shapes that MC's + # source still uses (e.g. `gateway sessions_send` -> `gateway call + # chat.send`). Bind-mounted so edits don't need an image rebuild. ── + - ./scripts/openclaw-cli-shim.py:/usr/local/lib/openclaw-cli-shim.py:ro + + extra_hosts: + - "host.docker.internal:host-gateway" + + # Dev mode is permissive — keep rootfs writable so next.js can scribble + # its dev cache, .swc artifacts, etc. The hardened production compose + # remains read-only. + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + security_opt: + - no-new-privileges:true + deploy: + resources: + limits: + memory: 4G + cpus: '4.0' + pids: 1024 + + networks: + - mc-net-dev + restart: unless-stopped + + # Run next directly instead of `pnpm dev` — the package.json `dev` script + # hardcodes `--hostname 127.0.0.1`, which would only listen on the loopback + # inside the container (unreachable from the host port mapping). Going + # through `pnpm exec` ensures we use the project's own next install. + command: ["pnpm", "exec", "next", "dev", "--hostname", "0.0.0.0", "--port", "${PORT:-3000}"] + +volumes: + mission-control_mc-data: + external: true + mc-next-dev: + +networks: + mc-net-dev: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 8336de2630..6f0ec4adf8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,53 @@ services: # If your gateway runs in another container, set this to the container name instead. - OPENCLAW_GATEWAY_HOST=${OPENCLAW_GATEWAY_HOST:-host.docker.internal} - OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT:-18789} + - OPENCLAW_GATEWAY_URL=${OPENCLAW_GATEWAY_URL:-ws://host.docker.internal:18789} + - OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN:-} + - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} + - TELEGRAM_NUMERIC_USER_ID=${TELEGRAM_NUMERIC_USER_ID:-} + - TELEGRAM_DM_POLICY=${TELEGRAM_DM_POLICY:-} + - TELEGRAM_ALLOW_FROM=${TELEGRAM_ALLOW_FROM:-} + - TELEGRAM_OWNER_ALLOW_FROM=${TELEGRAM_OWNER_ALLOW_FROM:-} + - OPENCLAW_TOOLS_PROFILE=${OPENCLAW_TOOLS_PROFILE:-coding} + - OPENCLAW_SECURITY_WORKSPACE_ONLY=${OPENCLAW_SECURITY_WORKSPACE_ONLY:-1} + - OPENCLAW_SECURITY_DENY_AUTOMATION=${OPENCLAW_SECURITY_DENY_AUTOMATION:-1} + - OPENCLAW_SECURITY_DENY_RUNTIME=${OPENCLAW_SECURITY_DENY_RUNTIME:-1} + - OPENCLAW_SECURITY_DENY_FS=${OPENCLAW_SECURITY_DENY_FS:-0} + - OPENCLAW_SECURITY_SANDBOX_ALL=${OPENCLAW_SECURITY_SANDBOX_ALL:-1} + - OPENCLAW_MESSAGES_GROUPCHAT_VISIBLE_REPLIES=${OPENCLAW_MESSAGES_GROUPCHAT_VISIBLE_REPLIES:-automatic} + - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-1} + - MC_ALLOWED_HOSTS=${MC_ALLOWED_HOSTS:-localhost,127.0.0.1,::1} + - OPENCLAW_STATE_DIR=/home/nextjs/.openclaw + - OPENCLAW_CONFIG_PATH=/home/nextjs/.openclaw/openclaw.json + # Host CLIs (~/.local/bin) come first; container-baked fallbacks second. + # Container HOME stays /home/nextjs; bind-mounts below project the host + # user's $HOME contents into that path. + - PATH=/home/nextjs/.local/bin:/home/nextjs/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - HOME=/home/nextjs + # MC's direct-API dispatch uses these (no gateway needed). + # Provider is picked by the agent's `dispatchModel` prefix: + # `claude-*` / `anthropic/*` → ANTHROPIC_API_KEY + # `gpt-*` / `o1-*` / `o3-*` / `openai/*` → OPENAI_API_KEY + # `local/*` / `ollama/*` / `lmstudio/*` / `litellm/*` + # → LOCAL_LLM_ENDPOINT (+ optional + # LOCAL_LLM_API_KEY) + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + # OpenAI-compatible local endpoint. LMStudio default is shown; point at + # Ollama (`http://host.docker.internal:11434/v1`) or a liteLLM proxy + # (`http://litellm:4000`) to fan out to multiple local backends. + - LOCAL_LLM_ENDPOINT=${LOCAL_LLM_ENDPOINT:-http://host.docker.internal:1234/v1} + - LOCAL_LLM_API_KEY=${LOCAL_LLM_API_KEY:-} + # How MC interacts with a session that may have a live host `claude` CLI: + # coexist (default) — both write to the same jsonl, race possible + # block-active — refuse with 409 if jsonl mtime < 60s ago + # nudge — coexist + bump mtime after reply + - MC_HOST_SESSION_MODE=${MC_HOST_SESSION_MODE:-coexist} + # /chat poll interval (ms) — fallback refresh when SSE drops on + # `session:` conversations. NEXT_PUBLIC_* is baked at BUILD time, so + # changing it requires `make rebuild`. Component default: 1500. + # 1000ms gives near-live updates as claude writes the jsonl line-by-line. + - NEXT_PUBLIC_CHAT_POLL_INTERVAL_MS=${NEXT_PUBLIC_CHAT_POLL_INTERVAL_MS:-1000} # ── Browser-side gateway connection (user's browser → gateway) ── # NEXT_PUBLIC_GATEWAY_HOST must be reachable from the user's browser, NOT from # inside the container. For local Docker: leave empty (auto-detected from browser). @@ -29,8 +76,32 @@ services: env_file: - path: .env required: false + - path: .env.openclaw + required: false + # Run as host UID:GID (defaults 1000:1000) so bind-mounted host configs + # ($HOME/.local/bin, $HOME/.claude, $HOME/.bun) are owned by a uid the + # container can read/write without chown. + user: "1000:1000" volumes: - mc-data:/app/.data + # Host configs are projected into /home/nextjs (container's $HOME) using + # the invoking shell's ${HOME}. Compose interpolates at `up` time, so the + # paths track whichever user runs the command. + - ${HOME}/.local/bin:/home/nextjs/.local/bin:rw + - ${HOME}/.local/share/claude:/home/nextjs/.local/share/claude:rw + - ${HOME}/.bun:/home/nextjs/.bun:rw + - ${HOME}/.claude:/home/nextjs/.claude:rw + - ${HOME}/.claude.json:/home/nextjs/.claude.json:rw + # Host repos / datasets visible to agents (mounted at the same absolute + # path so paths the user sees on the host work identically in container). + - /mnt:/mnt:rw + - ${HOME}:${HOME}:rw + # OpenClaw CLI state + identity used by Mission Control server-side CLI calls. + - ./.mc-openclaw:/home/nextjs/.openclaw:rw + # OpenClaw runtime source consumed by /usr/local/bin/openclaw shim in prod image. + - ./openclaw-src:/opt/openclaw-src:ro + # OpenClaw CLI shim for server-side gateway calls in production stack. + - ./scripts/openclaw-cli-shim.py:/home/nextjs/.local/bin/openclaw:ro # Optional: mount your OpenClaw state directory read-only so Mission Control # can read agent configs and memory. Uncomment and adjust the host path: # - ${OPENCLAW_HOME:-~/.openclaw}:/run/openclaw:ro @@ -52,9 +123,11 @@ services: deploy: resources: limits: - memory: 512M - cpus: '1.0' - pids: 256 + # Bumped from upstream default (512M). Next.js 16 + node-pty + + # task-dispatch loop OOMs at 512M when /chat opens a terminal. + memory: 2G + cpus: '2.0' + pids: 512 networks: - mc-net restart: unless-stopped