diff --git a/.env.example b/.env.example index d1bae01..4444c90 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,10 @@ # --- LLM ENGINE & MODEL SELECTION --- MODEL_LIST=gemma4:e4b,qwen3.6:35b-a3b-q4_K_M OLLAMA_KEEP_ALIVE=-1 +# KV cache quantization for large contexts (recommended on desktop GPUs): q8_0 or q4_0 +# OLLAMA_KV_CACHE_TYPE=q8_0 +# Reserve VRAM for display/compositor to avoid amdgpu allocation failures (bytes) +# OLLAMA_GPU_OVERHEAD=4294967296 # --- HARDWARE RESOURCES --- # GPU Vendor (e.g. "amd", "nvidia", "intel") @@ -25,3 +29,18 @@ HOST_USER_ID=1000 HOST_OPENCLAW_JSON_PATH=./openclaw/config/openclaw.json # Local path for the AI workspace (persistent storage for agent's work) HOST_WORKSPACE_PATH=./storage/workspace +# Local path for additional repositories/code visible to the agent +HOST_REPOS_PATH=. + +# --- OPTIONAL RUNTIME OVERRIDES --- +# Container engine used by ghostnet.sh (podman|docker) +CONTAINER_ENGINE=podman +# Optional explicit socket path for sandbox container creation API +# HOST_DOCKER_SOCKET_PATH=/run/user/1000/podman/podman.sock +# Optional explicit mount options for openclaw.json (default podman: :ro,Z | docker: :ro) +# HOST_OPENCLAW_CONFIG_MOUNT_OPTS=:ro,Z +# Optional explicit host workspace path seen by sandbox runtime +# OPENCLAW_SANDBOX_HOST_WORKSPACE=/absolute/path/to/storage/workspace + +# Sandbox base image is auto-built by ghostnet.sh as +# localhost/ghostnet-openclaw_sandbox:latest to include rootless apt defaults. diff --git a/compose.amd.yaml b/compose.amd.yaml index a544693..c14cd8f 100644 --- a/compose.amd.yaml +++ b/compose.amd.yaml @@ -25,7 +25,11 @@ services: - MODEL_LIST=${MODEL_LIST} - OLLAMA_KEEP_ALIVE=${OLLAMA_KEEP_ALIVE} - HSA_OVERRIDE_GFX_VERSION=11.0.0 - - OLLAMA_FLASH_ATTENTION=1 + - OLLAMA_FLASH_ATTENTION=${OLLAMA_FLASH_ATTENTION:-0} + - OLLAMA_KV_CACHE_TYPE=${OLLAMA_KV_CACHE_TYPE:-q8_0} + - OLLAMA_GPU_OVERHEAD=${OLLAMA_GPU_OVERHEAD:-4294967296} + - OLLAMA_MAX_LOADED_MODELS=${OLLAMA_MAX_LOADED_MODELS:-1} + - OLLAMA_NUM_PARALLEL=${OLLAMA_NUM_PARALLEL:-1} - http_proxy=http://ollama-proxy:3128 - https_proxy=http://ollama-proxy:3128 - no_proxy=localhost,127.0.0.1,0.0.0.0,ollama,openclaw-agent,tool-router,openclaw-ollama-bridge @@ -56,20 +60,25 @@ services: context: ./openclaw dockerfile: Containerfile container_name: openclaw - userns_mode: "keep-id" + user: "0:0" environment: - OPENCLAW_GATEWAY_MODE=local - OPENCLAW_GATEWAY_PASSWORD=${OPENCLAW_PASSWORD} + - OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json + - OPENCLAW_STATE_DIR=/home/node/.openclaw - OLLAMA_URL=http://127.0.0.1:11434 - OLLAMA_API_KEY=ollama-local - DOCKER_HOST=unix:///var/run/docker.sock + - OPENCLAW_SANDBOX_UIDSHIFT=${OPENCLAW_SANDBOX_UIDSHIFT} + - OPENCLAW_SANDBOX_HOST_WORKSPACE=${OPENCLAW_SANDBOX_HOST_WORKSPACE} + - OPENCLAW_SANDBOX_CONTAINER_WORKSPACE=${OPENCLAW_SANDBOX_CONTAINER_WORKSPACE} ports: - "8080:8080" volumes: - - ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json:Z - - ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace:Z,U - - /run/user/${HOST_USER_ID}/podman/podman.sock:/var/run/docker.sock:Z - - ${HOST_REPOS_PATH}:/repos:Z,U + - ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json${HOST_OPENCLAW_CONFIG_MOUNT_OPTS} + - ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace${HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS} + - ${HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock${HOST_DOCKER_SOCKET_MOUNT_OPTS} + - ${HOST_REPOS_PATH}:/repos${HOST_REPOS_MOUNT_OPTS} networks: - net.ghost.openclaw depends_on: @@ -154,7 +163,7 @@ services: restart: always user: "1000:1000" volumes: - - ./storage/searxng_valkey:/data:Z,U + - ./storage/searxng_valkey:/data${HOST_RW_MOUNT_OPTS} networks: - net.ghost.tools diff --git a/compose.intel.yaml b/compose.intel.yaml index f9d1c2d..8353a2b 100644 --- a/compose.intel.yaml +++ b/compose.intel.yaml @@ -29,22 +29,31 @@ services: # --- CORE AGENT (GATEWAY) --- agent: - image: ghcr.io/openclaw/openclaw:latest + build: + context: ./openclaw + dockerfile: Containerfile container_name: openclaw-agent + user: "0:0" depends_on: - ollama - userns_mode: "keep-id" environment: - OPENCLAW_GATEWAY_MODE=local - OPENCLAW_GATEWAY_PASSWORD=${OPENCLAW_PASSWORD} + - OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json + - OPENCLAW_STATE_DIR=/home/node/.openclaw - OLLAMA_URL=http://127.0.0.1:11434 - OLLAMA_API_KEY=ollama-local + - DOCKER_HOST=unix:///var/run/docker.sock + - OPENCLAW_SANDBOX_UIDSHIFT=${OPENCLAW_SANDBOX_UIDSHIFT} + - OPENCLAW_SANDBOX_HOST_WORKSPACE=${OPENCLAW_SANDBOX_HOST_WORKSPACE} + - OPENCLAW_SANDBOX_CONTAINER_WORKSPACE=${OPENCLAW_SANDBOX_CONTAINER_WORKSPACE} ports: - "8080:8080" volumes: - - ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json:Z - - ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace:Z,U - - ${HOST_REPOS_PATH}:/repos:Z,U + - ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json${HOST_OPENCLAW_CONFIG_MOUNT_OPTS} + - ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace${HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS} + - ${HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock${HOST_DOCKER_SOCKET_MOUNT_OPTS} + - ${HOST_REPOS_PATH}:/repos${HOST_REPOS_MOUNT_OPTS} networks: - net.ghost.openclaw diff --git a/compose.nvidia.yaml b/compose.nvidia.yaml index bd060eb..1a705d9 100644 --- a/compose.nvidia.yaml +++ b/compose.nvidia.yaml @@ -27,22 +27,31 @@ services: # --- CORE AGENT (GATEWAY) --- agent: - image: ghcr.io/openclaw/openclaw:latest + build: + context: ./openclaw + dockerfile: Containerfile container_name: openclaw-agent + user: "0:0" depends_on: - ollama - userns_mode: "keep-id" environment: - OPENCLAW_GATEWAY_MODE=local - OPENCLAW_GATEWAY_PASSWORD=${OPENCLAW_PASSWORD} + - OPENCLAW_CONFIG_PATH=/home/node/.openclaw/openclaw.json + - OPENCLAW_STATE_DIR=/home/node/.openclaw - OLLAMA_URL=http://127.0.0.1:11434 - OLLAMA_API_KEY=ollama-local + - DOCKER_HOST=unix:///var/run/docker.sock + - OPENCLAW_SANDBOX_UIDSHIFT=${OPENCLAW_SANDBOX_UIDSHIFT} + - OPENCLAW_SANDBOX_HOST_WORKSPACE=${OPENCLAW_SANDBOX_HOST_WORKSPACE} + - OPENCLAW_SANDBOX_CONTAINER_WORKSPACE=${OPENCLAW_SANDBOX_CONTAINER_WORKSPACE} ports: - "8080:8080" volumes: - - ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json:Z - - ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace:Z,U - - ${HOST_REPOS_PATH}:/repos:Z,U + - ${HOST_OPENCLAW_JSON_PATH}:/home/node/.openclaw/openclaw.json${HOST_OPENCLAW_CONFIG_MOUNT_OPTS} + - ${HOST_WORKSPACE_PATH}:/home/node/.openclaw/workspace${HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS} + - ${HOST_DOCKER_SOCKET_PATH}:/var/run/docker.sock${HOST_DOCKER_SOCKET_MOUNT_OPTS} + - ${HOST_REPOS_PATH}:/repos${HOST_REPOS_MOUNT_OPTS} networks: - net.ghost.openclaw diff --git a/ghostnet.sh b/ghostnet.sh index 346316f..dfa3a5d 100755 --- a/ghostnet.sh +++ b/ghostnet.sh @@ -1,67 +1,193 @@ #!/bin/bash -# Path to the .env file -ENV_FILE="./.env" +set -euo pipefail -# 1. Determine GPU_TYPE: Shell-Env first, then .env file -if [ -z "$GPU_TYPE" ]; then - if [ -f "$ENV_FILE" ]; then - GPU_TYPE=$(grep '^GPU_TYPE=' "$ENV_FILE" | cut -d '=' -f2) +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$PROJECT_DIR/.env" + +read_env_var() { + local key="$1" + if [ ! -f "$ENV_FILE" ]; then + return 0 fi + local line + line=$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 || true) + if [ -n "$line" ]; then + printf '%s' "${line#*=}" + fi +} + +resolve_abs_path() { + local raw_path="$1" + if [[ "$raw_path" = /* ]]; then + printf '%s' "$raw_path" + return + fi + if command -v realpath >/dev/null 2>&1; then + realpath -m "$PROJECT_DIR/$raw_path" + return + fi + (cd "$PROJECT_DIR" && printf '%s/%s\n' "$PWD" "${raw_path#./}") +} + +if [ -z "${GPU_TYPE:-}" ]; then + GPU_TYPE="$(read_env_var GPU_TYPE)" fi -# 2. Validation: Is the variable now set? -if [ -z "$GPU_TYPE" ]; then +if [ -z "${GPU_TYPE:-}" ]; then echo "ERROR: 'GPU_TYPE' is not defined!" - echo "💡 Please set it in your shell (export GPU_TYPE=amd) or create an .env file." + echo "Please set it in your shell (export GPU_TYPE=amd) or create an .env file." exit 1 fi -# 3. Validation: Is the value allowed? case "$GPU_TYPE" in amd|intel|nvidia) - COMPOSE_FILE="compose.$GPU_TYPE.yaml" + COMPOSE_FILE="$PROJECT_DIR/compose.$GPU_TYPE.yaml" ;; *) echo "ERROR: Invalid GPU_TYPE '$GPU_TYPE'!" echo "Allowed values are: amd, intel, nvidia" exit 1 + ;; esac -# 4. Check if YAML file exists if [ ! -f "$COMPOSE_FILE" ]; then echo "ERROR: The file '$COMPOSE_FILE' was not found!" exit 1 fi -# 5. Execute the command based on user input -COMMAND=$1 +if [ -z "${CONTAINER_ENGINE:-}" ]; then + CONTAINER_ENGINE="$(read_env_var CONTAINER_ENGINE)" +fi + +if [ -z "${CONTAINER_ENGINE:-}" ]; then + if command -v podman-compose >/dev/null 2>&1; then + CONTAINER_ENGINE="podman" + elif command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then + CONTAINER_ENGINE="docker" + else + echo "ERROR: Neither podman-compose nor docker compose is available." + exit 1 + fi +fi + +case "$CONTAINER_ENGINE" in + podman) + COMPOSE_CMD=(podman-compose) + ENGINE_CMD=(podman) + ;; + docker) + COMPOSE_CMD=(docker compose) + ENGINE_CMD=(docker) + ;; + *) + echo "ERROR: Invalid CONTAINER_ENGINE '$CONTAINER_ENGINE'. Use 'podman' or 'docker'." + exit 1 + ;; +esac + +if [ -z "${HOST_USER_ID:-}" ]; then + HOST_USER_ID="$(read_env_var HOST_USER_ID)" +fi +if [ -z "${HOST_USER_ID:-}" ]; then + HOST_USER_ID="$(id -u)" +fi +export HOST_USER_ID + +if [ -z "${HOST_WORKSPACE_PATH:-}" ]; then + HOST_WORKSPACE_PATH="$(read_env_var HOST_WORKSPACE_PATH)" +fi +if [ -z "${HOST_WORKSPACE_PATH:-}" ]; then + HOST_WORKSPACE_PATH="./storage/workspace" +fi +export HOST_WORKSPACE_PATH + +if [ -z "${HOST_REPOS_PATH:-}" ]; then + HOST_REPOS_PATH="$(read_env_var HOST_REPOS_PATH)" +fi +if [ -z "${HOST_REPOS_PATH:-}" ]; then + HOST_REPOS_PATH="$PROJECT_DIR" +fi +export HOST_REPOS_PATH + +HOST_WORKSPACE_ABS="$(resolve_abs_path "$HOST_WORKSPACE_PATH")" + +if [ "$CONTAINER_ENGINE" = "podman" ]; then + : "${HOST_DOCKER_SOCKET_PATH:=/run/user/${HOST_USER_ID}/podman/podman.sock}" + : "${HOST_DOCKER_SOCKET_MOUNT_OPTS:=:Z}" + : "${HOST_OPENCLAW_CONFIG_MOUNT_OPTS:=:ro,Z}" + : "${HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS:=:Z}" + : "${HOST_REPOS_MOUNT_OPTS:=:Z}" + : "${HOST_RW_MOUNT_OPTS:=:Z,U}" + : "${OPENCLAW_SANDBOX_UIDSHIFT:=1}" +else + : "${HOST_DOCKER_SOCKET_PATH:=/var/run/docker.sock}" + : "${HOST_DOCKER_SOCKET_MOUNT_OPTS:=}" + : "${HOST_OPENCLAW_CONFIG_MOUNT_OPTS:=:ro}" + : "${HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS:=}" + : "${HOST_REPOS_MOUNT_OPTS:=}" + : "${HOST_RW_MOUNT_OPTS:=}" + : "${OPENCLAW_SANDBOX_UIDSHIFT:=0}" +fi + +: "${OPENCLAW_SANDBOX_HOST_WORKSPACE:=$HOST_WORKSPACE_ABS}" +: "${OPENCLAW_SANDBOX_CONTAINER_WORKSPACE:=/home/node/.openclaw/workspace}" + +export HOST_DOCKER_SOCKET_PATH +export HOST_DOCKER_SOCKET_MOUNT_OPTS +export HOST_OPENCLAW_CONFIG_MOUNT_OPTS +export HOST_OPENCLAW_WORKSPACE_MOUNT_OPTS +export HOST_REPOS_MOUNT_OPTS +export HOST_RW_MOUNT_OPTS +export OPENCLAW_SANDBOX_UIDSHIFT +export OPENCLAW_SANDBOX_HOST_WORKSPACE +export OPENCLAW_SANDBOX_CONTAINER_WORKSPACE + +compose() { + "${COMPOSE_CMD[@]}" -f "$COMPOSE_FILE" "$@" +} + +build_sandbox_image() { + local sandbox_tag="localhost/ghostnet-openclaw_sandbox:latest" + echo "Building sandbox base image ($sandbox_tag) with ${ENGINE_CMD[*]}..." + "${ENGINE_CMD[@]}" build \ + -t "$sandbox_tag" \ + -f "$PROJECT_DIR/openclaw/sandbox/Containerfile" \ + "$PROJECT_DIR/openclaw/sandbox" +} + +COMMAND="${1:-}" + +cd "$PROJECT_DIR" case "$COMMAND" in up) - echo "🚀 Starting Ghostnet ($GPU_TYPE)..." - podman-compose -f "$COMPOSE_FILE" up -d + echo "Starting Ghostnet ($GPU_TYPE) with $CONTAINER_ENGINE..." + build_sandbox_image + compose up -d ;; down) - echo "🛑 Stopping Ghostnet ($GPU_TYPE)..." - podman-compose -f "$COMPOSE_FILE" down + echo "Stopping Ghostnet ($GPU_TYPE) with $CONTAINER_ENGINE..." + compose down ;; restart) - echo "🔄 Restarting Ghostnet ($GPU_TYPE)..." - podman-compose -f "$COMPOSE_FILE" restart + echo "Restarting Ghostnet ($GPU_TYPE) with $CONTAINER_ENGINE..." + compose restart ;; logs) - podman-compose -f "$COMPOSE_FILE" logs -f + compose logs -f ;; attached) - echo "🔍 Checking for running instances..." - podman-compose -f "$COMPOSE_FILE" down > /dev/null 2>&1 - echo "🔍 Starting Ghostnet in foreground ($GPU_TYPE)..." - podman-compose -f "$COMPOSE_FILE" up + echo "Checking for running instances..." + compose down > /dev/null 2>&1 || true + echo "Starting Ghostnet in foreground ($GPU_TYPE) with $CONTAINER_ENGINE..." + build_sandbox_image + compose up ;; *) echo "Usage: ./ghostnet.sh [up|down|restart|logs|attached]" echo "Current environment: $GPU_TYPE" + echo "Container engine: $CONTAINER_ENGINE" exit 1 ;; esac \ No newline at end of file diff --git a/ollama/modelfiles/qwen-coding-long.modelfile b/ollama/modelfiles/qwen-coding-long.modelfile new file mode 100644 index 0000000..45d0467 --- /dev/null +++ b/ollama/modelfiles/qwen-coding-long.modelfile @@ -0,0 +1,22 @@ +# Basis-Modell aus der Registry laden +FROM qwen3.6:27b + +# Long-Context-Profil fuer umfangreiche Edits +PARAMETER num_ctx 16384 +PARAMETER num_predict 4096 +PARAMETER temperature 0.2 + +# Dem Modell seine primĂ€re IdentitĂ€t verpassen +SYSTEM """ +Du bist ein hochentwickelter, autonomer KI-Coding-Agent innerhalb des OpenClaw-Frameworks. +Deine Aufgaben: +- Schreibe sauberen, performanten und fehlerfreien Python- und C#-Code. +- Analysiere Code-Strukturen tiefgehend und halte dich strikt an Best Practices. +- Gib prĂ€zise Antworten und vermeide unnoetiges Blabla. + +Arbeitsregeln: +- Simuliere keine Terminal-Ausgaben, keine "Let me check..."-Monologe und keine erfundenen Befehlslogs. +- Nutze verfĂŒgbare Tools/Befehle tatsĂ€chlich und berichte nur echte Ergebnisse. +- Wenn etwas nicht fertig ist, nenne den konkreten nĂ€chsten Schritt statt langer Zwischenkommentare. +- Antworte standardmĂ€ĂŸig auf Deutsch, außer der Nutzer fordert explizit etwas anderes. +""" diff --git a/ollama/modelfiles/qwen-coding.modelfile b/ollama/modelfiles/qwen-coding.modelfile index 485f57b..9db0243 100644 --- a/ollama/modelfiles/qwen-coding.modelfile +++ b/ollama/modelfiles/qwen-coding.modelfile @@ -2,14 +2,10 @@ FROM qwen3.6:27b # VRAM- und Kontext-Parameter direkt einbacken -PARAMETER num_ctx 32768 +PARAMETER num_ctx 8192 PARAMETER num_predict 4096 PARAMETER temperature 0.2 -# --- Stop-Token fĂŒr saubere Agenten-Kommunikation --- -PARAMETER stop <|im_end|> -PARAMETER stop <|im_start|> - # Dem Modell seine primĂ€re IdentitĂ€t verpassen SYSTEM """ Du bist ein hochentwickelter, autonomer KI-Coding-Agent innerhalb des OpenClaw-Frameworks. @@ -17,4 +13,10 @@ Deine Aufgaben: - Schreibe sauberen, performanten und fehlerfreien Python- und C#-Code. - Analysiere Code-Strukturen tiefgehend und halte dich strikt an Best Practices. - Gib prĂ€zise Antworten und vermeide unnötiges Blabla. + +Arbeitsregeln: +- Simuliere keine Terminal-Ausgaben, keine "Let me check..."-Monologe und keine erfundenen Befehlslogs. +- Nutze verfĂŒgbare Tools/Befehle tatsĂ€chlich und berichte nur echte Ergebnisse. +- Wenn etwas nicht fertig ist, nenne den konkreten nĂ€chsten Schritt statt langer Zwischenkommentare. +- Antworte standardmĂ€ĂŸig auf Deutsch, außer der Nutzer fordert explizit etwas anderes. """ diff --git a/openclaw/Containerfile b/openclaw/Containerfile index eddf150..71ba85a 100644 --- a/openclaw/Containerfile +++ b/openclaw/Containerfile @@ -6,4 +6,63 @@ RUN apt-get update \ && apt-get install -y docker.io \ && rm -rf /var/lib/apt/lists/* +RUN set -eu; \ + cat >/tmp/patch-openclaw-sandbox.js <<'EOF' +const fs = require("fs"); +const path = require("path"); + +const distDir = "/app/dist"; +const files = fs + .readdirSync(distDir) + .filter((name) => /^docker-.*\.js$/.test(name)) + .map((name) => path.join(distDir, name)); + +if (!files.length) { + throw new Error("No docker-*.js bundles found in /app/dist"); +} + +const bindNeedle = + 'return `${params.hostPath}:${params.containerPath}:${params.readOnly ? "ro,z" : "z"}`;'; +const bindReplacement = + 'const useUidShift = process.env.OPENCLAW_SANDBOX_UIDSHIFT === "1";\n' + + ' const hostWorkspaceRoot = process.env.OPENCLAW_SANDBOX_HOST_WORKSPACE?.trim();\n' + + ' const containerWorkspaceRoot = process.env.OPENCLAW_SANDBOX_CONTAINER_WORKSPACE?.trim() || "/home/node/.openclaw/workspace";\n' + + ' let hostPath = params.hostPath;\n' + + ' if (hostWorkspaceRoot && (hostPath === containerWorkspaceRoot || hostPath.startsWith(`${containerWorkspaceRoot}/`))) hostPath = `${hostWorkspaceRoot}${hostPath.slice(containerWorkspaceRoot.length)}`;\n' + + ' return `${hostPath}:${params.containerPath}:${params.readOnly ? useUidShift ? "ro,U,z" : "ro,z" : useUidShift ? "U,z" : "z"}`;'; + +let patchedFiles = 0; + +for (const filePath of files) { + const original = fs.readFileSync(filePath, "utf8"); + let patched = original; + + if (!patched.includes(bindNeedle)) { + continue; + } + + patched = patched.replace(bindNeedle, bindReplacement); + + if (patched === original) { + continue; + } + + fs.writeFileSync(filePath, patched, "utf8"); + patchedFiles += 1; +} + +if (!patchedFiles) { + throw new Error("OpenClaw dist patch failed: expected patterns not found in docker bundles"); +} + +console.log(`Patched ${patchedFiles} OpenClaw docker dist bundle(s).`); +EOF + +RUN node /tmp/patch-openclaw-sandbox.js \ + && rm -f /tmp/patch-openclaw-sandbox.js + +ENV OPENCLAW_SANDBOX_UIDSHIFT=0 +ENV OPENCLAW_SANDBOX_HOST_WORKSPACE= +ENV OPENCLAW_SANDBOX_CONTAINER_WORKSPACE=/home/node/.openclaw/workspace + USER node diff --git a/openclaw/config/openclaw.json b/openclaw/config/openclaw.json index 7b1cab9..d7b4d41 100644 --- a/openclaw/config/openclaw.json +++ b/openclaw/config/openclaw.json @@ -15,8 +15,10 @@ "mode": "all", "workspaceAccess": "rw", "docker": { - "image": "docker.io/nikolaik/python-nodejs:python3.14-nodejs26-slim", + "image": "localhost/ghostnet-openclaw_sandbox:latest", "network": "net.ghost.tools", + "readOnlyRoot": false, + "user": "root", "pidsLimit": 2048 } } @@ -30,4 +32,4 @@ } } } -} \ No newline at end of file +} diff --git a/openclaw/sandbox/Containerfile b/openclaw/sandbox/Containerfile new file mode 100644 index 0000000..aab8dcc --- /dev/null +++ b/openclaw/sandbox/Containerfile @@ -0,0 +1,11 @@ +FROM docker.io/nikolaik/python-nodejs:python3.14-nodejs26-slim + +USER root + +# Rootless Podman may block setgroups/seteuid transitions used by apt's default +# _apt sandbox user. Force apt to stay as root inside the sandbox container. +RUN mkdir -p /tmp/apt-archives/partial \ + && chmod 755 /tmp/apt-archives /tmp/apt-archives/partial \ + && printf 'APT::Sandbox::User "root";\nDir::Cache::archives "/tmp/apt-archives";\n' > /etc/apt/apt.conf.d/99rootless + +USER root diff --git a/scripts/benchmark-stack.sh b/scripts/benchmark-stack.sh new file mode 100755 index 0000000..4f2d403 --- /dev/null +++ b/scripts/benchmark-stack.sh @@ -0,0 +1,450 @@ +#!/usr/bin/env bash + +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$PROJECT_DIR" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +ok() { + echo -e "${GREEN}[OK]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +fail() { + echo -e "${RED}[FAIL]${NC} $1" + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "Benoetigtes Kommando fehlt: $1" +} + +usage() { + cat <<'EOF' +Usage: ./scripts/benchmark-stack.sh [options] + +Startet den Stack (optional), fuehrt eine reproduzierbare OpenClaw-Anfrage aus, +validiert den Output und zeigt Metriken (Laufzeit, Token, KV-/VRAM-Metriken). + +Optionen: + --model OpenClaw-Modell (default: ollama/qwen-coding-long:latest) + --lines Erwartete Zeilenanzahl LINE_1..LINE_n (default: 120) + --timeout-sec Timeout fuer den Agent-Call in Sekunden (default: 480) + --session-key Session-Key fuer den Lauf (default: auto) + --artifact-dir Zielverzeichnis fuer Ergebnisdateien (default: auto) + --no-start Stack nicht starten, nur vorhandenen Stack nutzen + --no-reload Modell vor Lauf nicht mit 'ollama stop' entladen + -h, --help Hilfe anzeigen + +Beispiel: + ./scripts/benchmark-stack.sh --model ollama/qwen-coding-long:latest --lines 140 +EOF +} + +strip_provider_prefix() { + local model="$1" + printf '%s' "${model#ollama/}" +} + +wait_for_container_running() { + local name="$1" + local timeout_sec="$2" + local waited=0 + while [ "$waited" -lt "$timeout_sec" ]; do + if podman ps --format '{{.Names}}' | grep -qx "$name"; then + return 0 + fi + sleep 1 + waited=$((waited + 1)) + done + return 1 +} + +wait_for_openclaw_ready() { + local timeout_sec="$1" + local waited=0 + while [ "$waited" -lt "$timeout_sec" ]; do + if podman exec openclaw sh -lc 'openclaw models list >/tmp/bench_models 2>&1' >/dev/null 2>&1; then + return 0 + fi + sleep 2 + waited=$((waited + 2)) + done + return 1 +} + +wait_for_ollama_ready() { + local timeout_sec="$1" + local waited=0 + while [ "$waited" -lt "$timeout_sec" ]; do + if podman exec ollama sh -lc 'ollama list >/tmp/bench_ollama_list 2>&1' >/dev/null 2>&1; then + return 0 + fi + sleep 2 + waited=$((waited + 2)) + done + return 1 +} + +extract_quoted_metric() { + local pattern="$1" + local key="$2" + local line + line="$(rg "$pattern" "$OLLAMA_LOG_FILE" | tail -n 1 || true)" + if [ -z "$line" ]; then + echo "n/a" + return + fi + + local value + value="$(printf '%s' "$line" | sed -E "s/.*${key}=\"([^\"]+)\".*/\\1/")" + if [ -z "$value" ] || [ "$value" = "$line" ]; then + echo "n/a" + else + echo "$value" + fi +} + +extract_unquoted_metric() { + local pattern="$1" + local sed_expr="$2" + local line + line="$(rg "$pattern" "$OLLAMA_LOG_FILE" | tail -n 1 || true)" + if [ -z "$line" ]; then + echo "n/a" + return + fi + + local value + value="$(printf '%s' "$line" | sed -E "$sed_expr")" + if [ -z "$value" ] || [ "$value" = "$line" ]; then + echo "n/a" + else + echo "$value" + fi +} + +MODEL="ollama/qwen-coding-long:latest" +EXPECTED_LINES=120 +TIMEOUT_SEC=480 +SESSION_KEY="" +ARTIFACT_DIR="" +START_STACK=1 +FORCE_RELOAD=1 + +while [ "$#" -gt 0 ]; do + case "$1" in + --model) + MODEL="$2" + shift 2 + ;; + --lines) + EXPECTED_LINES="$2" + shift 2 + ;; + --timeout-sec) + TIMEOUT_SEC="$2" + shift 2 + ;; + --session-key) + SESSION_KEY="$2" + shift 2 + ;; + --artifact-dir) + ARTIFACT_DIR="$2" + shift 2 + ;; + --no-start) + START_STACK=0 + shift + ;; + --no-reload) + FORCE_RELOAD=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "Unbekannte Option: $1" + ;; + esac +done + +if ! [[ "$EXPECTED_LINES" =~ ^[0-9]+$ ]] || [ "$EXPECTED_LINES" -lt 1 ]; then + fail "--lines muss eine positive Ganzzahl sein" +fi + +if ! [[ "$TIMEOUT_SEC" =~ ^[0-9]+$ ]] || [ "$TIMEOUT_SEC" -lt 1 ]; then + fail "--timeout-sec muss eine positive Ganzzahl sein" +fi + +need_cmd podman +need_cmd jq +need_cmd rg +need_cmd sed +need_cmd awk +need_cmd timeout + +if [ -z "$ARTIFACT_DIR" ]; then + TS="$(date +%Y%m%d-%H%M%S)" + ARTIFACT_DIR="$PROJECT_DIR/storage/workspace/benchmarks/$TS" +fi +mkdir -p "$ARTIFACT_DIR" + +JSON_OUT_FILE="$ARTIFACT_DIR/agent-result.json" +ASSISTANT_TEXT_FILE="$ARTIFACT_DIR/assistant-output.txt" +OLLAMA_LOG_FILE="$ARTIFACT_DIR/ollama-since.log" +STATS_BEFORE_FILE="$ARTIFACT_DIR/container-stats-before.txt" +STATS_AFTER_FILE="$ARTIFACT_DIR/container-stats-after.txt" +REQUEST_ERR_FILE="$ARTIFACT_DIR/request-stderr.log" +SUMMARY_FILE="$ARTIFACT_DIR/summary.txt" + +if [ -z "$SESSION_KEY" ]; then + SESSION_KEY="agent:main:benchmark:$(date +%s)" +fi + +MODEL_OLLAMA="$(strip_provider_prefix "$MODEL")" + +echo "=== Ghostnet Benchmark ===" +echo "Projekt: $PROJECT_DIR" +echo "Model: $MODEL" +echo "Session key: $SESSION_KEY" +echo "Erwartete Zeilen: $EXPECTED_LINES" +echo "Timeout: ${TIMEOUT_SEC}s" +echo "Artefakte: $ARTIFACT_DIR" + +if [ "$START_STACK" -eq 1 ]; then + echo + echo "[1/6] Starte Stack via ./ghostnet.sh up ..." + ./ghostnet.sh up +else + echo + echo "[1/6] Stack-Start uebersprungen (--no-start)" +fi + +echo "[2/6] Warte auf Container-Readiness ..." +wait_for_container_running ollama 180 || fail "Container ollama ist nicht running" +wait_for_container_running openclaw 180 || fail "Container openclaw ist nicht running" +wait_for_ollama_ready 120 || fail "Ollama API wurde nicht rechtzeitig bereit" +wait_for_openclaw_ready 180 || fail "OpenClaw wurde nicht rechtzeitig bereit" +ok "Container und APIs sind bereit" + +if [ "$FORCE_RELOAD" -eq 1 ]; then + echo "[3/6] Entlade Modell fuer frische KV/VRAM-Load-Metriken ..." + podman exec ollama sh -lc "ollama stop '$MODEL_OLLAMA' >/dev/null 2>&1 || true" +else + echo "[3/6] Modell-Entladung uebersprungen (--no-reload)" +fi + +echo "[4/6] Fuehre Benchmark-Anfrage aus ..." +PROMPT="Gib exakt ${EXPECTED_LINES} Zeilen aus. Jede Zeile muss exakt den Text LINE_ haben, mit aufsteigender Nummerierung von 1 bis ${EXPECTED_LINES}. Kein weiterer Text." +SINCE_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + +podman stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}' ollama openclaw > "$STATS_BEFORE_FILE" 2>&1 || true + +START_NS="$(date +%s%N)" +REQUEST_RC=0 +if ! timeout "${TIMEOUT_SEC}s" podman exec openclaw openclaw agent \ + --session-key "$SESSION_KEY" \ + --model "$MODEL" \ + --message "$PROMPT" \ + --json > "$JSON_OUT_FILE" 2> "$REQUEST_ERR_FILE"; then + REQUEST_RC=$? +fi +END_NS="$(date +%s%N)" +WALL_MS=$(( (END_NS - START_NS) / 1000000 )) + +podman logs --since "$SINCE_UTC" ollama > "$OLLAMA_LOG_FILE" 2>&1 || true +podman stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}' ollama openclaw > "$STATS_AFTER_FILE" 2>&1 || true + +HAS_VALID_JSON=0 +if [ -s "$JSON_OUT_FILE" ] && jq -e . "$JSON_OUT_FILE" >/dev/null 2>&1; then + HAS_VALID_JSON=1 +fi +if [ "$HAS_VALID_JSON" -ne 1 ] && [ "$REQUEST_RC" -eq 0 ]; then + REQUEST_RC=65 +fi + +REQUEST_ERROR_MSG="" +if [ "$REQUEST_RC" -ne 0 ]; then + REQUEST_ERROR_MSG="$(tail -n 20 "$REQUEST_ERR_FILE" 2>/dev/null | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g' | sed -E 's/^ //; s/ $//')" + if [ -z "$REQUEST_ERROR_MSG" ]; then + if [ "$HAS_VALID_JSON" -ne 1 ]; then + REQUEST_ERROR_MSG="Agent lieferte kein valides JSON" + else + REQUEST_ERROR_MSG="Agent-Lauf fehlgeschlagen ohne stderr-Ausgabe" + fi + fi + warn "Agent-Lauf fehlgeschlagen (rc=$REQUEST_RC): $REQUEST_ERROR_MSG" +fi + +echo "[5/6] Validiere Antwortformat ..." +ASSISTANT_TEXT="" +if [ "$HAS_VALID_JSON" -eq 1 ]; then + ASSISTANT_TEXT="$(jq -r '.result.payloads[0].text // ""' "$JSON_OUT_FILE")" +fi +printf '%s' "$ASSISTANT_TEXT" > "$ASSISTANT_TEXT_FILE" + +ACTUAL_LINES=() +if [ -n "$ASSISTANT_TEXT" ]; then + mapfile -t ACTUAL_LINES < "$ASSISTANT_TEXT_FILE" + for i in "${!ACTUAL_LINES[@]}"; do + ACTUAL_LINES[$i]="${ACTUAL_LINES[$i]%$'\r'}" + done +fi + +VALIDATION_OK=1 +VALIDATION_DETAIL="OK" + +if [ "$REQUEST_RC" -ne 0 ]; then + VALIDATION_OK=0 + VALIDATION_DETAIL="Agent-Lauf fehlgeschlagen (rc=$REQUEST_RC)" +elif [ "${#ACTUAL_LINES[@]}" -ne "$EXPECTED_LINES" ]; then + VALIDATION_OK=0 + VALIDATION_DETAIL="Zeilenanzahl abweichend: erwartet=$EXPECTED_LINES, erhalten=${#ACTUAL_LINES[@]}" +else + for ((idx = 1; idx <= EXPECTED_LINES; idx++)); do + expected="LINE_${idx}" + actual="${ACTUAL_LINES[$((idx - 1))]}" + if [ "$actual" != "$expected" ]; then + VALIDATION_OK=0 + VALIDATION_DETAIL="Mismatch in Zeile $idx: erwartet='$expected', erhalten='$actual'" + break + fi + done +fi + +if [ "$VALIDATION_OK" -eq 1 ]; then + ok "Output validiert: exakt LINE_1..LINE_$EXPECTED_LINES" +else + warn "Output-Validierung fehlgeschlagen: $VALIDATION_DETAIL" +fi + +echo "[6/6] Sammle und berechne Metriken ..." +DURATION_MS="n/a" +USAGE_INPUT="n/a" +USAGE_OUTPUT="n/a" +USAGE_TOTAL="n/a" +PROMPT_TOKENS="n/a" +CONTEXT_TOKENS="n/a" +STOP_REASON="n/a" +FINISH_REASON="n/a" + +if [ "$HAS_VALID_JSON" -eq 1 ]; then + DURATION_MS="$(jq -r '.result.meta.durationMs // "n/a"' "$JSON_OUT_FILE")" + USAGE_INPUT="$(jq -r '.result.meta.agentMeta.usage.input // "n/a"' "$JSON_OUT_FILE")" + USAGE_OUTPUT="$(jq -r '.result.meta.agentMeta.usage.output // "n/a"' "$JSON_OUT_FILE")" + USAGE_TOTAL="$(jq -r '.result.meta.agentMeta.usage.total // "n/a"' "$JSON_OUT_FILE")" + PROMPT_TOKENS="$(jq -r '.result.meta.agentMeta.promptTokens // "n/a"' "$JSON_OUT_FILE")" + CONTEXT_TOKENS="$(jq -r '.result.meta.agentMeta.contextTokens // "n/a"' "$JSON_OUT_FILE")" + STOP_REASON="$(jq -r '.result.meta.completion.stopReason // "n/a"' "$JSON_OUT_FILE")" + FINISH_REASON="$(jq -r '.result.meta.completion.finishReason // "n/a"' "$JSON_OUT_FILE")" +fi + +OUTPUT_TOKENS_PER_SEC="n/a" +if [[ "$DURATION_MS" =~ ^[0-9]+$ ]] && [ "$DURATION_MS" -gt 0 ] && [[ "$USAGE_OUTPUT" =~ ^[0-9]+$ ]]; then + OUTPUT_TOKENS_PER_SEC="$(awk "BEGIN { printf \"%.2f\", ($USAGE_OUTPUT * 1000) / $DURATION_MS }")" +fi + +END2END_TOKENS_PER_SEC="n/a" +if [[ "$WALL_MS" =~ ^[0-9]+$ ]] && [ "$WALL_MS" -gt 0 ] && [[ "$USAGE_OUTPUT" =~ ^[0-9]+$ ]]; then + END2END_TOKENS_PER_SEC="$(awk "BEGIN { printf \"%.2f\", ($USAGE_OUTPUT * 1000) / $WALL_MS }")" +fi + +GPU_AVAILABLE="$(extract_quoted_metric 'msg="gpu memory"' 'available')" +GPU_FREE="$(extract_quoted_metric 'msg="gpu memory"' 'free')" +GPU_OVERHEAD="$(extract_quoted_metric 'msg="gpu memory"' 'overhead')" +KV_GPU="$(extract_quoted_metric 'msg="kv cache" device=ROCm0' 'size')" +KV_CPU="$(extract_quoted_metric 'msg="kv cache" device=CPU' 'size')" +MODEL_GPU="$(extract_quoted_metric 'msg="model weights" device=ROCm0' 'size')" +MODEL_CPU="$(extract_quoted_metric 'msg="model weights" device=CPU' 'size')" +COMPUTE_GPU="$(extract_quoted_metric 'msg="compute graph" device=ROCm0' 'size')" +COMPUTE_CPU="$(extract_quoted_metric 'msg="compute graph" device=CPU' 'size')" +TOTAL_MEMORY="$(extract_quoted_metric 'msg="total memory"' 'size')" +KV_SIZE_TOKENS="$(extract_unquoted_metric 'KvSize:[0-9]+' 's/.*KvSize:([0-9]+).*/\1/')" +RUNNER_START_SEC="$(extract_unquoted_metric 'llama runner started in [0-9]+(\.[0-9]+)? seconds' 's/.*llama runner started in ([0-9]+(\.[0-9]+)?) seconds.*/\1/')" + +OLLAMA_ENV="$(podman exec ollama sh -lc 'echo "KV_CACHE=$OLLAMA_KV_CACHE_TYPE GPU_OVERHEAD=$OLLAMA_GPU_OVERHEAD KEEP_ALIVE=$OLLAMA_KEEP_ALIVE FLASH_ATTN=$OLLAMA_FLASH_ATTENTION MAX_LOADED=$OLLAMA_MAX_LOADED_MODELS NUM_PARALLEL=$OLLAMA_NUM_PARALLEL"' 2>/dev/null || true)" + +JOURNAL_HITS="n/a" +if command -v journalctl >/dev/null 2>&1; then + JOURNAL_HITS="$(journalctl -b --since "$SINCE_UTC" --no-pager 2>/dev/null | rg -i 'amdgpu|MESA: error|graphics reset|drm|failed to allocate a buffer|not enough memory for command submission' | wc -l | tr -d ' ' || true)" + [ -n "$JOURNAL_HITS" ] || JOURNAL_HITS="0" +fi + +{ + echo "=== Ghostnet Benchmark Summary ===" + echo "Model: $MODEL" + echo "Session key: $SESSION_KEY" + echo "Request exit code: $REQUEST_RC" + if [ "$REQUEST_RC" -ne 0 ]; then + echo "Request error: $REQUEST_ERROR_MSG" + fi + echo "Prompt line target: $EXPECTED_LINES" + echo "Validation: $([ "$VALIDATION_OK" -eq 1 ] && echo PASS || echo FAIL)" + echo "Validation detail: $VALIDATION_DETAIL" + echo + echo "Latenz und Token" + echo "- end_to_end_wall_ms: $WALL_MS" + echo "- openclaw_duration_ms: $DURATION_MS" + echo "- usage_input_tokens: $USAGE_INPUT" + echo "- usage_output_tokens: $USAGE_OUTPUT" + echo "- usage_total_tokens: $USAGE_TOTAL" + echo "- prompt_tokens: $PROMPT_TOKENS" + echo "- context_tokens: $CONTEXT_TOKENS" + echo "- stop_reason: $STOP_REASON" + echo "- finish_reason: $FINISH_REASON" + echo "- output_tokens_per_sec_provider: $OUTPUT_TOKENS_PER_SEC" + echo "- output_tokens_per_sec_end_to_end: $END2END_TOKENS_PER_SEC" + echo + echo "Ollama runtime" + echo "- env: $OLLAMA_ENV" + echo "- gpu_available: $GPU_AVAILABLE" + echo "- gpu_free: $GPU_FREE" + echo "- gpu_overhead: $GPU_OVERHEAD" + echo "- kv_cache_gpu: $KV_GPU" + echo "- kv_cache_cpu: $KV_CPU" + echo "- kv_size_tokens: $KV_SIZE_TOKENS" + echo "- model_weights_gpu: $MODEL_GPU" + echo "- model_weights_cpu: $MODEL_CPU" + echo "- compute_graph_gpu: $COMPUTE_GPU" + echo "- compute_graph_cpu: $COMPUTE_CPU" + echo "- total_model_memory: $TOTAL_MEMORY" + echo "- model_load_time_sec: $RUNNER_START_SEC" + echo + echo "Stabilitaet" + echo "- journal_gpu_related_hits_since_start: $JOURNAL_HITS" + echo + echo "Artefakte" + echo "- agent_json: $JSON_OUT_FILE" + echo "- assistant_text: $ASSISTANT_TEXT_FILE" + echo "- ollama_log_since_start: $OLLAMA_LOG_FILE" + echo "- stats_before: $STATS_BEFORE_FILE" + echo "- stats_after: $STATS_AFTER_FILE" + echo "- request_stderr: $REQUEST_ERR_FILE" +} | tee "$SUMMARY_FILE" + +echo +echo "Container-Stats vor Lauf:" +cat "$STATS_BEFORE_FILE" +echo +echo "Container-Stats nach Lauf:" +cat "$STATS_AFTER_FILE" + +if [ "$REQUEST_RC" -eq 0 ] && [ "$VALIDATION_OK" -eq 1 ]; then + ok "Benchmark erfolgreich abgeschlossen" + exit 0 +fi + +warn "Benchmark beendet, aber Validierung ist fehlgeschlagen" +exit 2 \ No newline at end of file diff --git a/scripts/chat.sh b/scripts/chat.sh new file mode 100755 index 0000000..91a7e5e --- /dev/null +++ b/scripts/chat.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +podman exec -it openclaw openclaw chat \ No newline at end of file diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..51e2bd2 --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Quick end-to-end smoke test for OpenClaw sandbox runtime. +# Prerequisites: running stack and jq installed on host. + +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$PROJECT_DIR" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +ok() { + echo -e "${GREEN}[OK]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +fail() { + echo -e "${RED}[FAIL]${NC} $1" + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "Benoetigtes Kommando fehlt: $1" +} + +need_cmd podman +need_cmd jq + +OPENCLAW_NAME="openclaw" + +if ! podman ps --format '{{.Names}}' | grep -qx "$OPENCLAW_NAME"; then + fail "Container $OPENCLAW_NAME laeuft nicht. Starte den Stack zuerst mit ./ghostnet.sh up" +fi +ok "Container $OPENCLAW_NAME laeuft" + +if podman exec "$OPENCLAW_NAME" sh -lc 'docker version >/tmp/smoke_dv 2>&1'; then + ok "Docker/Podman Socket im OpenClaw-Container erreichbar" +else + echo "--- docker version output ---" + podman exec "$OPENCLAW_NAME" sh -lc 'cat /tmp/smoke_dv || true' + fail "Socket im OpenClaw-Container nicht erreichbar" +fi + +if podman exec "$OPENCLAW_NAME" sh -lc 'touch /home/node/.openclaw/workspace/.smoke_openclaw_write && rm -f /home/node/.openclaw/workspace/.smoke_openclaw_write'; then + ok "OpenClaw kann in den Primary Workspace schreiben" +else + fail "OpenClaw kann NICHT in den Primary Workspace schreiben" +fi + +AGENT_PROMPT='Fuehre in der Sandbox genau diesen Befehl aus: sh -lc "touch /workspace/smoke_mount_probe.txt && echo SMOKE_SANDBOX_OK". Antworte nur mit dem Kommando-Output.' +AGENT_OUT_FILE="/tmp/openclaw_smoke_agent.json" + +if ! podman exec "$OPENCLAW_NAME" sh -lc "openclaw agent --session-key agent:main:smoke-test -m '$AGENT_PROMPT' --json" > "$AGENT_OUT_FILE" 2>&1; then + echo "--- agent output ---" + cat "$AGENT_OUT_FILE" + fail "Agent-Turn fehlgeschlagen (Sandbox evtl. nicht aktiv)" +fi + +if ! grep -q 'SMOKE_SANDBOX_OK' "$AGENT_OUT_FILE"; then + echo "--- agent output ---" + cat "$AGENT_OUT_FILE" + fail "Sandbox-Befehl hat erwartete Rueckgabe nicht geliefert" +fi +ok "Sandbox-Befehl erfolgreich ausgefuehrt" + +SBOX_NAME="$(podman ps -a --format '{{.Names}}' | grep '^openclaw-sbx-' | head -n 1 || true)" +[ -n "$SBOX_NAME" ] || fail "Kein openclaw-sbx-* Container gefunden" +ok "Sandbox-Container gefunden: $SBOX_NAME" + +MOUNTS_JSON="$(podman inspect "$SBOX_NAME" --format '{{json .Mounts}}')" +WORKSPACE_SOURCE="$(echo "$MOUNTS_JSON" | jq -r '.[] | select(.Destination=="/workspace") | .Source' | head -n 1)" + +[ -n "$WORKSPACE_SOURCE" ] || fail "Kein /workspace Mount im Sandbox-Container gefunden" +echo "Sandbox /workspace Source: $WORKSPACE_SOURCE" + +EXPECTED_SOURCE="$(realpath -m "${HOST_WORKSPACE_PATH:-./storage/workspace}")" +if [ "$WORKSPACE_SOURCE" = "$EXPECTED_SOURCE" ]; then + ok "Sandbox-Mount zeigt auf erwarteten Host-Workspace" +else + warn "Sandbox-Mount weicht von erwartetem Host-Workspace ab" + echo "Erwartet: $EXPECTED_SOURCE" +fi + +if podman exec "$SBOX_NAME" sh -lc 'test -f /workspace/smoke_mount_probe.txt'; then + ok "Probe-Datei existiert im Sandbox-Workspace" +else + fail "Probe-Datei fehlt im Sandbox-Workspace" +fi + +if podman exec "$SBOX_NAME" sh -lc 'apt-get update >/tmp/smoke_apt_update 2>&1'; then + ok "apt-get update in Sandbox erfolgreich" +else + echo "--- apt-get update output ---" + podman exec "$SBOX_NAME" sh -lc 'tail -n 40 /tmp/smoke_apt_update || true' + fail "apt-get update in Sandbox fehlgeschlagen" +fi + +if podman exec "$SBOX_NAME" sh -lc 'apt-get install -y jq >/tmp/smoke_apt_install 2>&1'; then + if podman exec "$SBOX_NAME" sh -lc 'command -v jq >/dev/null 2>&1'; then + ok "apt-get install funktioniert in Sandbox (jq installiert)" + else + fail "apt-get install meldete Erfolg, jq ist aber nicht vorhanden" + fi +else + echo "--- apt-get install output ---" + podman exec "$SBOX_NAME" sh -lc 'tail -n 60 /tmp/smoke_apt_install || true' + fail "apt-get install in Sandbox fehlgeschlagen" +fi + +ok "Smoke-Test erfolgreich abgeschlossen"